Skip to content

Commit 8d3612f

Browse files
committed
Allow json(..., sort_keys=True) to handle mixed keys. (https://bugs.python.org/issue25457)
1 parent d904c23 commit 8d3612f

2 files changed

Lines changed: 99 additions & 62 deletions

File tree

‎Lib/json/encoder.py‎

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,30 @@ def _iterencode_list(lst, _current_indent_level):
331331
del markers[markerid]
332332

333333
def _iterencode_dict(dct, _current_indent_level):
334+
def _coerce_key(key):
335+
if isinstance(key, str):
336+
return key
337+
# JavaScript is weakly typed for these, so it makes sense to
338+
# also allow them. Many encoders seem to do something like this.
339+
if isinstance(key, float):
340+
# see comment for int/float in _make_iterencode
341+
return _floatstr(key)
342+
if key is True:
343+
return 'true'
344+
if key is False:
345+
return 'false'
346+
if key is None:
347+
return 'null'
348+
if isinstance(key, int):
349+
# see comment for int/float in _make_iterencode
350+
return _intstr(key)
351+
352+
if _skipkeys:
353+
return None
354+
else:
355+
raise TypeError(f'keys must be str, int, float, bool or None, '
356+
f'not {key.__class__.__name__}')
357+
334358
if not dct:
335359
yield '{}'
336360
return
@@ -349,32 +373,17 @@ def _iterencode_dict(dct, _current_indent_level):
349373
newline_indent = None
350374
item_separator = _item_separator
351375
first = True
376+
377+
# Coerce keys to strings (or None if _skipkeys)
378+
items = ((_coerce_key(k), v) for (k,v) in dct.items())
379+
352380
if _sort_keys:
353-
items = sorted(dct.items(), key=lambda kv: kv[0])
354-
else:
355-
items = dct.items()
381+
items = sorted((k,v) for (k,v) in items if k is not None)
382+
356383
for key, value in items:
357-
if isinstance(key, str):
358-
pass
359-
# JavaScript is weakly typed for these, so it makes sense to
360-
# also allow them. Many encoders seem to do something like this.
361-
elif isinstance(key, float):
362-
# see comment for int/float in _make_iterencode
363-
key = _floatstr(key)
364-
elif key is True:
365-
key = 'true'
366-
elif key is False:
367-
key = 'false'
368-
elif key is None:
369-
key = 'null'
370-
elif isinstance(key, int):
371-
# see comment for int/float in _make_iterencode
372-
key = _intstr(key)
373-
elif _skipkeys:
384+
if key is None:
385+
# If specified, skip keys that we weren't able to coerce to strings
374386
continue
375-
else:
376-
raise TypeError(f'keys must be str, int, float, bool or None, '
377-
f'not {key.__class__.__name__}')
378387
if first:
379388
first = False
380389
else:

‎Modules/_json.c‎

Lines changed: 67 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,7 @@ encoder_listencode_dict(PyEncoderObject *s, _PyAccu *acc,
15621562
PyObject *ident = NULL;
15631563
PyObject *it = NULL;
15641564
PyObject *items;
1565+
PyObject *coerced_items;
15651566
PyObject *item = NULL;
15661567
Py_ssize_t idx;
15671568

@@ -1607,55 +1608,81 @@ encoder_listencode_dict(PyEncoderObject *s, _PyAccu *acc,
16071608
items = PyMapping_Items(dct);
16081609
if (items == NULL)
16091610
goto bail;
1610-
if (s->sort_keys && PyList_Sort(items) < 0) {
1611-
Py_DECREF(items);
1611+
1612+
coerced_items = PyList_New(0);
1613+
it = PyObject_GetIter(items);
1614+
Py_DECREF(items);
1615+
if (it == NULL)
1616+
goto bail;
1617+
1618+
while ((item = PyIter_Next(it)) != NULL) {
1619+
PyObject *key, *value, *coerced_item;
1620+
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
1621+
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
1622+
goto bail;
1623+
}
1624+
key = PyTuple_GET_ITEM(item, 0);
1625+
if (PyUnicode_Check(key)) {
1626+
Py_INCREF(key);
1627+
kstr = key;
1628+
}
1629+
else if (PyFloat_Check(key)) {
1630+
kstr = encoder_encode_float(s, key);
1631+
if (kstr == NULL)
1632+
goto bail;
1633+
}
1634+
else if (key == Py_True || key == Py_False || key == Py_None) {
1635+
/* This must come before the PyLong_Check because
1636+
True and False are also 1 and 0.*/
1637+
kstr = _encoded_const(key);
1638+
if (kstr == NULL)
1639+
goto bail;
1640+
}
1641+
else if (PyLong_Check(key)) {
1642+
kstr = PyLong_Type.tp_str(key);
1643+
if (kstr == NULL) {
1644+
goto bail;
1645+
}
1646+
}
1647+
else if (s->skipkeys) {
1648+
Py_DECREF(item);
1649+
continue;
1650+
}
1651+
else {
1652+
PyErr_Format(PyExc_TypeError,
1653+
"keys must be str, int, float, bool or None, "
1654+
"not %.100s", key->ob_type->tp_name);
1655+
goto bail;
1656+
}
1657+
1658+
value = PyTuple_GET_ITEM(item, 1);
1659+
coerced_item = PyTuple_Pack(2, kstr, value);
1660+
if (coerced_item == NULL) {
1661+
goto bail;
1662+
}
1663+
/* Append instead of set because skipkeys=True may
1664+
"shrink" the number of items */
1665+
if (-1 == PyList_Append(coerced_items, coerced_item))
1666+
goto bail;
1667+
}
1668+
1669+
if (s->sort_keys && PyList_Sort(coerced_items) < 0) {
1670+
Py_DECREF(coerced_items);
16121671
goto bail;
16131672
}
1614-
it = PyObject_GetIter(items);
1615-
Py_DECREF(items);
1673+
it = PyObject_GetIter(coerced_items);
1674+
Py_DECREF(coerced_items);
16161675
if (it == NULL)
16171676
goto bail;
16181677
idx = 0;
16191678
while ((item = PyIter_Next(it)) != NULL) {
1620-
PyObject *encoded, *key, *value;
1679+
PyObject *encoded, *value;
16211680
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
16221681
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
16231682
goto bail;
16241683
}
1625-
key = PyTuple_GET_ITEM(item, 0);
1626-
if (PyUnicode_Check(key)) {
1627-
Py_INCREF(key);
1628-
kstr = key;
1629-
}
1630-
else if (PyFloat_Check(key)) {
1631-
kstr = encoder_encode_float(s, key);
1632-
if (kstr == NULL)
1633-
goto bail;
1634-
}
1635-
else if (key == Py_True || key == Py_False || key == Py_None) {
1636-
/* This must come before the PyLong_Check because
1637-
True and False are also 1 and 0.*/
1638-
kstr = _encoded_const(key);
1639-
if (kstr == NULL)
1640-
goto bail;
1641-
}
1642-
else if (PyLong_Check(key)) {
1643-
kstr = PyLong_Type.tp_str(key);
1644-
if (kstr == NULL) {
1645-
goto bail;
1646-
}
1647-
}
1648-
else if (s->skipkeys) {
1649-
Py_DECREF(item);
1650-
continue;
1651-
}
1652-
else {
1653-
PyErr_Format(PyExc_TypeError,
1654-
"keys must be str, int, float, bool or None, "
1655-
"not %.100s", key->ob_type->tp_name);
1656-
goto bail;
1657-
}
1658-
1684+
kstr = PyTuple_GET_ITEM(item, 0);
1685+
16591686
if (idx) {
16601687
if (_PyAccu_Accumulate(acc, s->item_separator))
16611688
goto bail;
@@ -1703,6 +1730,7 @@ encoder_listencode_dict(PyEncoderObject *s, _PyAccu *acc,
17031730
Py_XDECREF(item);
17041731
Py_XDECREF(kstr);
17051732
Py_XDECREF(ident);
1733+
Py_XDECREF(coerced_items);
17061734
return -1;
17071735
}
17081736

0 commit comments

Comments
 (0)