diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 8768b63a3f80417..9fc822812db9769 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -325,6 +325,37 @@ 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 int.__repr__(key) + + if _skipkeys: + return None + else: + raise TypeError(f'keys must be str, int, float, bool or None, ' + f'not {key.__class__.__name__}') + + def _coerce_items(items): + for (k,v) in items: + k = _coerce_key(k) # Coerce or throw + if k is None: + continue + yield (k,v) + if not dct: yield '{}' return @@ -342,32 +373,14 @@ def _iterencode_dict(dct, _current_indent_level): newline_indent = None item_separator = _item_separator first = True + + # Coerce keys to strings + items = _coerce_items(dct.items()) + if _sort_keys: - items = sorted(dct.items()) - else: - items = dct.items() + items = sorted(items) + 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 = int.__repr__(key) - elif _skipkeys: - continue - else: - raise TypeError(f'keys must be str, int, float, bool or None, ' - f'not {key.__class__.__name__}') if first: first = False if newline_indent is not None: diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 5bc03085e60a3d3..2c414e74c87bb80 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -48,7 +48,7 @@ def test_encode_truefalse(self): '{"false": true, "true": false}') self.assertEqual(self.dumps( {2: 3.0, 4.0: 5, False: 1, 6: True}, sort_keys=True), - '{"false": 1, "2": 3.0, "4.0": 5, "6": true}') + '{"2": 3.0, "4.0": 5, "6": true, "false": 1}') # Issue 16228: Crash on encoding resized list def test_encode_mutated(self): diff --git a/Lib/test/test_json/test_speedups.py b/Lib/test/test_json/test_speedups.py index 0b22a0bf4b95387..b5c1d3088abafe4 100644 --- a/Lib/test/test_json/test_speedups.py +++ b/Lib/test/test_json/test_speedups.py @@ -78,10 +78,6 @@ def test(name): self.assertRaises(ZeroDivisionError, test, 'allow_nan') self.assertRaises(ZeroDivisionError, test, 'sort_keys') - def test_unsortable_keys(self): - with self.assertRaises(TypeError): - self.json.encoder.JSONEncoder(sort_keys=True).encode({'a': 1, 1: 'a'}) - def test_current_indent_level(self): enc = self.json.encoder.c_make_encoder( markers=None, diff --git a/Modules/_json.c b/Modules/_json.c index 3a724a3e72b185b..8ac21663d86963d 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1665,14 +1665,12 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer, } } +/* Convert the key to the string used in the output. Return 1 and set + *pkeystr on success, 0 if the key is skipped, and -1 on error. */ static int -encoder_encode_key_value(PyEncoderObject *s, PyUnicodeWriter *writer, bool *first, - PyObject *dct, PyObject *key, PyObject *value, - Py_ssize_t indent_level, PyObject *indent_cache, - PyObject *item_separator) +encoder_coerce_key(PyEncoderObject *s, PyObject *key, PyObject **pkeystr) { - PyObject *keystr = NULL; - int rv; + PyObject *keystr; if (PyUnicode_Check(key)) { keystr = Py_NewRef(key); @@ -1701,6 +1699,67 @@ encoder_encode_key_value(PyEncoderObject *s, PyUnicodeWriter *writer, bool *firs if (keystr == NULL) { return -1; } + *pkeystr = keystr; + return 1; +} + +/* Return a new list of (key string, value) pairs, with the keys converted + and the skipped keys omitted. */ +static PyObject * +encoder_coerce_items(PyEncoderObject *s, PyObject *items) +{ + PyObject *coerced = PyList_New(0); + if (coerced == NULL) { + return NULL; + } + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(items); i++) { + PyObject *item = Py_NewRef(PyList_GET_ITEM(items, i)); + if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) { + PyErr_SetString(PyExc_ValueError, "items must return 2-tuples"); + Py_DECREF(item); + goto bail; + } + PyObject *keystr; + int res = encoder_coerce_key(s, PyTuple_GET_ITEM(item, 0), &keystr); + if (res <= 0) { + Py_DECREF(item); + if (res < 0) { + goto bail; + } + continue; /* the key is skipped */ + } + PyObject *pair = PyTuple_Pack(2, keystr, PyTuple_GET_ITEM(item, 1)); + Py_DECREF(keystr); + Py_DECREF(item); + if (pair == NULL) { + goto bail; + } + int rv = PyList_Append(coerced, pair); + Py_DECREF(pair); + if (rv < 0) { + goto bail; + } + } + return coerced; + +bail: + Py_DECREF(coerced); + return NULL; +} + +static int +encoder_encode_key_value(PyEncoderObject *s, PyUnicodeWriter *writer, bool *first, + PyObject *dct, PyObject *key, PyObject *value, + Py_ssize_t indent_level, PyObject *indent_cache, + PyObject *item_separator) +{ + PyObject *keystr; + int rv; + + rv = encoder_coerce_key(s, key, &keystr); + if (rv <= 0) { + return rv; + } if (*first) { *first = false; @@ -1837,10 +1896,19 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer, if (s->sort_keys || !PyAnyDict_CheckExact(dct)) { PyObject *items = PyMapping_Items(dct); - if (items == NULL || (s->sort_keys && PyList_Sort(items) < 0)) { - Py_XDECREF(items); + if (items == NULL) { goto bail; } + if (s->sort_keys) { + /* Keys are converted to strings before sorting, so that + a dict with mixed key types can be sorted, and the keys + which are skipped do not participate in the sorting. */ + Py_SETREF(items, encoder_coerce_items(s, items)); + if (items == NULL || PyList_Sort(items) < 0) { + Py_XDECREF(items); + goto bail; + } + } int result; Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(items);