Skip to content

Commit

Permalink
Allow json(..., sort_keys=True) to handle mixed keys. (https://bugs.p…
Browse files Browse the repository at this point in the history
  • Loading branch information
jheiv committed Jun 29, 2018
1 parent d904c23 commit 8d3612f
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 62 deletions.
55 changes: 32 additions & 23 deletions Lib/json/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,30 @@ def _iterencode_list(lst, _current_indent_level):
del markers[markerid]

def _iterencode_dict(dct, _current_indent_level):
def _coerce_key(key):
if isinstance(key, str):
return key
# JavaScript is weakly typed for these, so it makes sense to
# also allow them. Many encoders seem to do something like this.
if isinstance(key, float):
# see comment for int/float in _make_iterencode
return _floatstr(key)
if key is True:
return 'true'
if key is False:
return 'false'
if key is None:
return 'null'
if isinstance(key, int):
# see comment for int/float in _make_iterencode
return _intstr(key)

if _skipkeys:
return None
else:
raise TypeError(f'keys must be str, int, float, bool or None, '
f'not {key.__class__.__name__}')

if not dct:
yield '{}'
return
Expand All @@ -349,32 +373,17 @@ def _iterencode_dict(dct, _current_indent_level):
newline_indent = None
item_separator = _item_separator
first = True

# Coerce keys to strings (or None if _skipkeys)
items = ((_coerce_key(k), v) for (k,v) in dct.items())

if _sort_keys:
items = sorted(dct.items(), key=lambda kv: kv[0])
else:
items = dct.items()
items = sorted((k,v) for (k,v) in items if k is not None)

for key, value in items:
if isinstance(key, str):
pass
# JavaScript is weakly typed for these, so it makes sense to
# also allow them. Many encoders seem to do something like this.
elif isinstance(key, float):
# see comment for int/float in _make_iterencode
key = _floatstr(key)
elif key is True:
key = 'true'
elif key is False:
key = 'false'
elif key is None:
key = 'null'
elif isinstance(key, int):
# see comment for int/float in _make_iterencode
key = _intstr(key)
elif _skipkeys:
if key is None:
# If specified, skip keys that we weren't able to coerce to strings
continue
else:
raise TypeError(f'keys must be str, int, float, bool or None, '
f'not {key.__class__.__name__}')
if first:
first = False
else:
Expand Down
106 changes: 67 additions & 39 deletions Modules/_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,7 @@ encoder_listencode_dict(PyEncoderObject *s, _PyAccu *acc,
PyObject *ident = NULL;
PyObject *it = NULL;
PyObject *items;
PyObject *coerced_items;
PyObject *item = NULL;
Py_ssize_t idx;

Expand Down Expand Up @@ -1607,55 +1608,81 @@ encoder_listencode_dict(PyEncoderObject *s, _PyAccu *acc,
items = PyMapping_Items(dct);
if (items == NULL)
goto bail;
if (s->sort_keys && PyList_Sort(items) < 0) {
Py_DECREF(items);

coerced_items = PyList_New(0);
it = PyObject_GetIter(items);
Py_DECREF(items);
if (it == NULL)
goto bail;

while ((item = PyIter_Next(it)) != NULL) {
PyObject *key, *value, *coerced_item;
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
goto bail;
}
key = PyTuple_GET_ITEM(item, 0);
if (PyUnicode_Check(key)) {
Py_INCREF(key);
kstr = key;
}
else if (PyFloat_Check(key)) {
kstr = encoder_encode_float(s, key);
if (kstr == NULL)
goto bail;
}
else if (key == Py_True || key == Py_False || key == Py_None) {
/* This must come before the PyLong_Check because
True and False are also 1 and 0.*/
kstr = _encoded_const(key);
if (kstr == NULL)
goto bail;
}
else if (PyLong_Check(key)) {
kstr = PyLong_Type.tp_str(key);
if (kstr == NULL) {
goto bail;
}
}
else if (s->skipkeys) {
Py_DECREF(item);
continue;
}
else {
PyErr_Format(PyExc_TypeError,
"keys must be str, int, float, bool or None, "
"not %.100s", key->ob_type->tp_name);
goto bail;
}

value = PyTuple_GET_ITEM(item, 1);
coerced_item = PyTuple_Pack(2, kstr, value);
if (coerced_item == NULL) {
goto bail;
}
/* Append instead of set because skipkeys=True may
"shrink" the number of items */
if (-1 == PyList_Append(coerced_items, coerced_item))
goto bail;
}

if (s->sort_keys && PyList_Sort(coerced_items) < 0) {
Py_DECREF(coerced_items);
goto bail;
}
it = PyObject_GetIter(items);
Py_DECREF(items);
it = PyObject_GetIter(coerced_items);
Py_DECREF(coerced_items);
if (it == NULL)
goto bail;
idx = 0;
while ((item = PyIter_Next(it)) != NULL) {
PyObject *encoded, *key, *value;
PyObject *encoded, *value;
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
goto bail;
}
key = PyTuple_GET_ITEM(item, 0);
if (PyUnicode_Check(key)) {
Py_INCREF(key);
kstr = key;
}
else if (PyFloat_Check(key)) {
kstr = encoder_encode_float(s, key);
if (kstr == NULL)
goto bail;
}
else if (key == Py_True || key == Py_False || key == Py_None) {
/* This must come before the PyLong_Check because
True and False are also 1 and 0.*/
kstr = _encoded_const(key);
if (kstr == NULL)
goto bail;
}
else if (PyLong_Check(key)) {
kstr = PyLong_Type.tp_str(key);
if (kstr == NULL) {
goto bail;
}
}
else if (s->skipkeys) {
Py_DECREF(item);
continue;
}
else {
PyErr_Format(PyExc_TypeError,
"keys must be str, int, float, bool or None, "
"not %.100s", key->ob_type->tp_name);
goto bail;
}

kstr = PyTuple_GET_ITEM(item, 0);

if (idx) {
if (_PyAccu_Accumulate(acc, s->item_separator))
goto bail;
Expand Down Expand Up @@ -1703,6 +1730,7 @@ encoder_listencode_dict(PyEncoderObject *s, _PyAccu *acc,
Py_XDECREF(item);
Py_XDECREF(kstr);
Py_XDECREF(ident);
Py_XDECREF(coerced_items);
return -1;
}

Expand Down

0 comments on commit 8d3612f

Please sign in to comment.