From 1ccc65f74284e4e57632a81048531ece71195420 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Tue, 26 Mar 2024 03:41:43 +0000 Subject: [PATCH 1/9] Add `convert_keys` param to JSONEncoder to support passing non-simple dictionary keys through the `.default()` method for conversion to a supported type. --- Doc/library/json.rst | 12 ++++++++---- Lib/json/encoder.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index c82ff9dc325b4c..1e87b929b984bd 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -148,15 +148,19 @@ Basic Usage .. function:: dump(obj, fp, *, skipkeys=False, ensure_ascii=True, \ check_circular=True, allow_nan=True, cls=None, \ indent=None, separators=None, default=None, \ - sort_keys=False, **kw) + sort_keys=False, convert_keys=False, **kw) Serialize *obj* as a JSON formatted stream to *fp* (a ``.write()``-supporting :term:`file-like object`) using this :ref:`conversion table `. - If *skipkeys* is true (default: ``False``), then dict keys that are not - of a basic type (:class:`str`, :class:`int`, :class:`float`, :class:`bool`, - ``None``) will be skipped instead of raising a :exc:`TypeError`. + *skipkeys* and *convert_keys* control how keys that are not of a basic type + (:class:`str`, :class:`int`, :class:`float`, :class:`bool`, ``None``) are + handled. If *skipkeys* is true (default: ``False``), then such keys will be + skipped. Otherwise, if *convert_keys* is true (default: ``False``), such + keys are converted to a supported type by calling the encoder's + :meth:`~JSONEncoder.default()` method. If `.default()` returns an + unsupported type, or *convert_keys* is False, :exc:`TypeError` is raised. The :mod:`json` module always produces :class:`str` objects, not :class:`bytes` objects. Therefore, ``fp.write()`` must support :class:`str` diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 597849eca0524a..764e91a464cd2b 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -104,12 +104,15 @@ class JSONEncoder(object): key_separator = ': ' def __init__(self, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, - indent=None, separators=None, default=None): + indent=None, separators=None, default=None, convert_keys=False): """Constructor for JSONEncoder, with sensible defaults. - If skipkeys is false, then it is a TypeError to attempt - encoding of keys that are not str, int, float or None. If - skipkeys is True, such items are simply skipped. + Dict keys in JSON must be str, int, float or None. skipkeys and + convert_keys control how keys that are not one of these types are + handled. If skipkeys is True, then those items are simply skipped. + Otherwise, if convert_keys is True, the keys will be passed to + ``.default()`` to be converted. If convert_keys is False, or + ``.default()`` returns an unsupported type, TypeError is raised. If ensure_ascii is true, the output is guaranteed to be str objects with all incoming non-ASCII characters escaped. If @@ -157,6 +160,7 @@ def __init__(self, *, skipkeys=False, ensure_ascii=True, self.item_separator = ',' if default is not None: self.default = default + self.convert_keys = convert_keys def default(self, o): """Implement this method in a subclass such that it returns @@ -254,11 +258,12 @@ def floatstr(o, allow_nan=self.allow_nan, _iterencode = _make_iterencode( markers, self.default, _encoder, self.indent, floatstr, self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot) + self.skipkeys, self.convert_keys, _one_shot) return _iterencode(o, 0) def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, - _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + _key_separator, _item_separator, _sort_keys, _skipkeys, _convert_keys, + _one_shot, ## HACK: hand-optimized bytecode; turn globals into locals ValueError=ValueError, dict=dict, @@ -373,6 +378,11 @@ def _iterencode_dict(dct, _current_indent_level): key = _intstr(key) elif _skipkeys: continue + elif _convert_keys: + key = _default(key) + if not isinstance(key, (int, bool, float, str, None)): + raise TypeError(f'keys must be str, int, float, bool ' + f'or None, not {key.__class__.__name__}') else: raise TypeError(f'keys must be str, int, float, bool or None, ' f'not {key.__class__.__name__}') From 9cb2095ef2a6a3e32e39a8190b6de074e00295c0 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Thu, 28 Mar 2024 04:18:09 +0000 Subject: [PATCH 2/9] Implement JSONEncoder `convert_keys` option to allow passing dictionary keys that are not of a JSON-supported simple type through the encoder's .default() method. --- Lib/json/encoder.py | 10 ++--- Lib/test/test_json/test_dump.py | 10 +++++ Modules/_json.c | 73 +++++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 764e91a464cd2b..485ec19522fe17 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -107,7 +107,7 @@ def __init__(self, *, skipkeys=False, ensure_ascii=True, indent=None, separators=None, default=None, convert_keys=False): """Constructor for JSONEncoder, with sensible defaults. - Dict keys in JSON must be str, int, float or None. skipkeys and + Dict keys in JSON must be str, int, float, bool, or None. skipkeys and convert_keys control how keys that are not one of these types are handled. If skipkeys is True, then those items are simply skipped. Otherwise, if convert_keys is True, the keys will be passed to @@ -253,7 +253,7 @@ def floatstr(o, allow_nan=self.allow_nan, _iterencode = c_make_encoder( markers, self.default, _encoder, self.indent, self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, self.allow_nan) + self.skipkeys, self.allow_nan, self.convert_keys) else: _iterencode = _make_iterencode( markers, self.default, _encoder, self.indent, floatstr, @@ -376,13 +376,13 @@ def _iterencode_dict(dct, _current_indent_level): elif isinstance(key, int): # see comment for int/float in _make_iterencode key = _intstr(key) - elif _skipkeys: - continue elif _convert_keys: key = _default(key) - if not isinstance(key, (int, bool, float, str, None)): + if not isinstance(key, (int, bool, float, str, type(None))): raise TypeError(f'keys must be str, int, float, bool ' f'or None, not {key.__class__.__name__}') + elif _skipkeys: + continue else: raise TypeError(f'keys must be str, int, float, bool or None, ' f'not {key.__class__.__name__}') diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 13b40020781bae..4cfdd8d65803d0 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -22,6 +22,16 @@ def test_dump_skipkeys(self): self.assertIn('valid_key', o) self.assertNotIn(b'invalid_key', o) + def test_dump_convert_keys(self): + v = {b'bytes_key': False, 'valid_key': True} + with self.assertRaises(TypeError): + self.json.dumps(v) + + s = self.json.dumps(v, convert_keys=True, default=lambda b: b.hex(':')) + o = self.json.loads(s) + self.assertIn('valid_key', o) + self.assertIn('62:79:74:65:73:5f:6b:65:79', o) + def test_encode_truefalse(self): self.assertEqual(self.dumps( {True: False, False: True}, sort_keys=True), diff --git a/Modules/_json.c b/Modules/_json.c index c55299899e77fe..3d380c50374f25 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -46,6 +46,7 @@ typedef struct _PyEncoderObject { PyObject *item_separator; char sort_keys; char skipkeys; + char convert_keys; int allow_nan; PyCFunction fast_encode; } PyEncoderObject; @@ -59,6 +60,7 @@ static PyMemberDef encoder_members[] = { {"item_separator", _Py_T_OBJECT, offsetof(PyEncoderObject, item_separator), Py_READONLY, "item_separator"}, {"sort_keys", Py_T_BOOL, offsetof(PyEncoderObject, sort_keys), Py_READONLY, "sort_keys"}, {"skipkeys", Py_T_BOOL, offsetof(PyEncoderObject, skipkeys), Py_READONLY, "skipkeys"}, + {"convert_keys", Py_T_BOOL, offsetof(PyEncoderObject, convert_keys), Py_READONLY, "convert_keys"}, {NULL} }; @@ -1205,17 +1207,17 @@ static PyType_Spec PyScannerType_spec = { static PyObject * encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; + static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", "convert_keys", NULL}; PyEncoderObject *s; PyObject *markers, *defaultfn, *encoder, *indent, *key_separator; PyObject *item_separator; - int sort_keys, skipkeys, allow_nan; + int sort_keys, skipkeys, allow_nan, convert_keys; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppp:make_encoder", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUpppp:make_encoder", kwlist, &markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator, - &sort_keys, &skipkeys, &allow_nan)) + &sort_keys, &skipkeys, &allow_nan, &convert_keys)) return NULL; if (markers != Py_None && !PyDict_Check(markers)) { @@ -1238,6 +1240,7 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) s->sort_keys = sort_keys; s->skipkeys = skipkeys; s->allow_nan = allow_nan; + s->convert_keys = convert_keys; s->fast_encode = NULL; if (PyCFunction_Check(s->encoder)) { @@ -1475,6 +1478,68 @@ encoder_encode_key_value(PyEncoderObject *s, _PyUnicodeWriter *writer, bool *fir else if (PyLong_Check(key)) { keystr = PyLong_Type.tp_repr(key); } + else if (s->convert_keys) { + PyObject *ident = NULL; + PyObject *newobj = NULL; + int rv; + if (s->markers != Py_None) + { + int has_key; + ident = PyLong_FromVoidPtr(key); + if (ident == NULL) + return -1; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) + { + if (has_key != -1) + Py_XDECREF(ident); + return -1; + } + Py_XDECREF(ident); + } + + newobj = PyObject_CallOneArg(s->defaultfn, key); + if (newobj == NULL) + { + Py_XDECREF(ident); + return -1; + } + + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) + { + Py_DECREF(newobj); + Py_XDECREF(ident); + return -1; + } + rv = encoder_listencode_obj(s, writer, newobj, indent_level); + _Py_LeaveRecursiveCall(); + + Py_DECREF(newobj); + if (rv) + { + Py_XDECREF(ident); + return -1; + } + if (ident != NULL) + { + if (PyDict_DelItem(s->markers, ident)) + { + Py_XDECREF(ident); + return -1; + } + Py_XDECREF(ident); + } + + if (!(PyUnicode_Check(newobj) || PyFloat_Check(newobj) || newobj == Py_True + || newobj == Py_False || newobj == Py_None || PyLong_Check(newobj))) { + PyErr_Format(PyExc_TypeError, + "keys must be str, int, float, bool or None, " + "not %.100s", + Py_TYPE(newobj)->tp_name); + return -1; + } + return rv; + } else if (s->skipkeys) { return 0; } From c7dd4576b27685bbc75fd776a7e393903a596d4e Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 11:03:42 -0600 Subject: [PATCH 3/9] Clean up and reorganize. Ensure `skipkeys` takes preference over `convert_keys` as documented. --- Lib/json/encoder.py | 2 +- Lib/test/test_json/test_dump.py | 7 ++- Lib/test/test_json/test_speedups.py | 6 +-- Modules/_json.c | 75 +++++++++-------------------- 4 files changed, 33 insertions(+), 57 deletions(-) diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 485ec19522fe17..240136518ee337 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -378,7 +378,7 @@ def _iterencode_dict(dct, _current_indent_level): key = _intstr(key) elif _convert_keys: key = _default(key) - if not isinstance(key, (int, bool, float, str, type(None))): + if key is not None and not isinstance(key, (int, bool, float, str)): raise TypeError(f'keys must be str, int, float, bool ' f'or None, not {key.__class__.__name__}') elif _skipkeys: diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 4cfdd8d65803d0..06a0f21b1b3936 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -27,7 +27,12 @@ def test_dump_convert_keys(self): with self.assertRaises(TypeError): self.json.dumps(v) - s = self.json.dumps(v, convert_keys=True, default=lambda b: b.hex(':')) + def default(val): + if isinstance(val, bytes): + return val.hex(':') + return str(val) + + s = self.json.dumps(v, convert_keys=True, default=default) o = self.json.loads(s) self.assertIn('valid_key', o) self.assertIn('62:79:74:65:73:5f:6b:65:79', o) diff --git a/Lib/test/test_json/test_speedups.py b/Lib/test/test_json/test_speedups.py index 682014cfd5b344..097a0d449a093f 100644 --- a/Lib/test/test_json/test_speedups.py +++ b/Lib/test/test_json/test_speedups.py @@ -45,7 +45,7 @@ def bad_encoder1(*args): return None enc = self.json.encoder.c_make_encoder(None, lambda obj: str(obj), bad_encoder1, None, ': ', ', ', - False, False, False) + False, False, False, False) with self.assertRaises(TypeError): enc('spam', 4) with self.assertRaises(TypeError): @@ -55,7 +55,7 @@ def bad_encoder2(*args): 1/0 enc = self.json.encoder.c_make_encoder(None, lambda obj: str(obj), bad_encoder2, None, ': ', ', ', - False, False, False) + False, False, False, False) with self.assertRaises(ZeroDivisionError): enc('spam', 4) @@ -66,7 +66,7 @@ def test_bad_markers_argument_to_encoder(self): r'make_encoder\(\) argument 1 must be dict or None, not int', ): self.json.encoder.c_make_encoder(1, None, None, None, ': ', ', ', - False, False, False) + False, False, False, False) def test_bad_bool_args(self): def test(name): diff --git a/Modules/_json.c b/Modules/_json.c index 3d380c50374f25..be66f04c7ae7ba 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1478,70 +1478,41 @@ encoder_encode_key_value(PyEncoderObject *s, _PyUnicodeWriter *writer, bool *fir else if (PyLong_Check(key)) { keystr = PyLong_Type.tp_repr(key); } + else if (s->skipkeys) { + return 0; + } else if (s->convert_keys) { - PyObject *ident = NULL; - PyObject *newobj = NULL; - int rv; - if (s->markers != Py_None) - { - int has_key; - ident = PyLong_FromVoidPtr(key); - if (ident == NULL) - return -1; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) - { - if (has_key != -1) - Py_XDECREF(ident); - return -1; - } - Py_XDECREF(ident); - } - + PyObject *newobj; newobj = PyObject_CallOneArg(s->defaultfn, key); - if (newobj == NULL) - { - Py_XDECREF(ident); + if (newobj == NULL) { return -1; } - - if (_Py_EnterRecursiveCall(" while encoding a JSON object")) - { + if (!(newobj == Py_True || newobj == Py_False || newobj == Py_None || PyUnicode_Check(newobj) + || PyFloat_Check(newobj) || PyLong_Check(newobj))) { Py_DECREF(newobj); - Py_XDECREF(ident); + PyErr_Format(PyExc_TypeError, + "keys must be str, int, float, bool or None, " + "not %.100s", + Py_TYPE(newobj)->tp_name); return -1; } - rv = encoder_listencode_obj(s, writer, newobj, indent_level); - _Py_LeaveRecursiveCall(); - Py_DECREF(newobj); - if (rv) - { - Py_XDECREF(ident); - return -1; + if (newobj == Py_None || newobj == Py_True || newobj == Py_False) { + keystr = _encoded_const(key); } - if (ident != NULL) - { - if (PyDict_DelItem(s->markers, ident)) - { - Py_XDECREF(ident); + else if (PyUnicode_Check(newobj)) { + keystr = Py_NewRef(newobj); + } + else if (PyLong_Check(newobj)) { + keystr = PyLong_Type.tp_repr(newobj); + if (keystr == NULL) return -1; - } - Py_XDECREF(ident); } - - if (!(PyUnicode_Check(newobj) || PyFloat_Check(newobj) || newobj == Py_True - || newobj == Py_False || newobj == Py_None || PyLong_Check(newobj))) { - PyErr_Format(PyExc_TypeError, - "keys must be str, int, float, bool or None, " - "not %.100s", - Py_TYPE(newobj)->tp_name); - return -1; + else if (PyFloat_Check(newobj)) { + keystr = encoder_encode_float(s, newobj); + if (keystr == NULL) + return -1; } - return rv; - } - else if (s->skipkeys) { - return 0; } else { PyErr_Format(PyExc_TypeError, From dd29b85e531325986f1dd886f2bb56a48b0c604b Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 11:49:54 -0600 Subject: [PATCH 4/9] Update module-level dump functions to support JSONEncoder `convert_keys` parameter. --- Lib/json/__init__.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index ed2c74771ea87d..329f4cd746045b 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -119,13 +119,16 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, - default=None, sort_keys=False, **kw): + default=None, sort_keys=False, convert_keys=False, **kw): """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a ``.write()``-supporting file-like object). - If ``skipkeys`` is true then ``dict`` keys that are not basic types - (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped - instead of raising a ``TypeError``. + Dict keys in JSON must be str, int, float, bool, or None. ``skipkeys`` and + ``convert_keys`` control how keys that are not one of these types are + handled. If ``skipkeys`` is True, then those items are simply skipped. + Otherwise, if ``convert_keys`` is True, the keys will be passed to + ``.default()`` to be converted. If ``convert_keys`` is False, or + ``.default()`` returns an unsupported type, ``TypeError`` is raised. If ``ensure_ascii`` is false, then the strings written to ``fp`` can contain non-ASCII characters if they appear in strings contained in @@ -165,7 +168,7 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, if (not skipkeys and ensure_ascii and check_circular and allow_nan and cls is None and indent is None and separators is None and - default is None and not sort_keys and not kw): + default is None and not sort_keys and not convert_keys and not kw): iterable = _default_encoder.iterencode(obj) else: if cls is None: @@ -173,7 +176,8 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, indent=indent, separators=separators, - default=default, sort_keys=sort_keys, **kw).iterencode(obj) + default=default, sort_keys=sort_keys, convert_keys=convert_keys, + **kw).iterencode(obj) # could accelerate with writelines in some versions of Python, at # a debuggability cost for chunk in iterable: @@ -182,12 +186,15 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, - default=None, sort_keys=False, **kw): + default=None, sort_keys=False, convert_keys=False, **kw): """Serialize ``obj`` to a JSON formatted ``str``. - If ``skipkeys`` is true then ``dict`` keys that are not basic types - (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped - instead of raising a ``TypeError``. + Dict keys in JSON must be str, int, float, bool, or None. ``skipkeys`` and + ``convert_keys`` control how keys that are not one of these types are + handled. If ``skipkeys`` is True, then those items are simply skipped. + Otherwise, if ``convert_keys`` is True, the keys will be passed to + ``.default()`` to be converted. If ``convert_keys`` is False, or + ``.default()`` returns an unsupported type, ``TypeError`` is raised. If ``ensure_ascii`` is false, then the return value can contain non-ASCII characters if they appear in strings contained in ``obj``. Otherwise, all @@ -227,7 +234,7 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, if (not skipkeys and ensure_ascii and check_circular and allow_nan and cls is None and indent is None and separators is None and - default is None and not sort_keys and not kw): + default is None and not sort_keys and not convert_keys and not kw): return _default_encoder.encode(obj) if cls is None: cls = JSONEncoder @@ -235,7 +242,7 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, indent=indent, separators=separators, default=default, sort_keys=sort_keys, - **kw).encode(obj) + convert_keys=convert_keys, **kw).encode(obj) _default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None) From 3fff0e39d625afc99a7f9a25e956484015d0c5a0 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 11:50:03 -0600 Subject: [PATCH 5/9] Add NEWS item. --- .../Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst diff --git a/Misc/NEWS.d/next/Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst b/Misc/NEWS.d/next/Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst new file mode 100644 index 00000000000000..f6c143c5fabdcf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst @@ -0,0 +1,3 @@ +Add `convert_keys` parameter to JSON encoding, to allow passing dict keys that +are not supported types in JSON through the encoder's `.default()` method. +Patch by Charles Cazabon. From efbc7536f349efbba63dee56dadcde50c733d0a4 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 12:01:51 -0600 Subject: [PATCH 6/9] Fix NEWS filename. --- ....u7KmA9.rst => 2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Library/{2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst => 2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst b/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2024-03-30-11-42-00-13.gh-issue-117391.u7KmA9.rst rename to Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst From 626d140bd70e950ecd85af563fc3eea8549da488 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 13:32:31 -0600 Subject: [PATCH 7/9] Fix incorrect markup in docs. --- Doc/library/json.rst | 2 +- .../Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index 1e87b929b984bd..66e379d3ef2802 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -159,7 +159,7 @@ Basic Usage handled. If *skipkeys* is true (default: ``False``), then such keys will be skipped. Otherwise, if *convert_keys* is true (default: ``False``), such keys are converted to a supported type by calling the encoder's - :meth:`~JSONEncoder.default()` method. If `.default()` returns an + :meth:`JSONEncoder.default` method. If :meth:`JSONEncoder.default` returns an unsupported type, or *convert_keys* is False, :exc:`TypeError` is raised. The :mod:`json` module always produces :class:`str` objects, not diff --git a/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst b/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst index f6c143c5fabdcf..e5250a0d76dd30 100644 --- a/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst +++ b/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst @@ -1,3 +1,4 @@ Add `convert_keys` parameter to JSON encoding, to allow passing dict keys that -are not supported types in JSON through the encoder's `.default()` method. +are not supported types in JSON through the encoder's +:meth:`json.JSONEncoder.default()` method. Patch by Charles Cazabon. From b00ab885315d8fdf43e91f67c41d415d87d0b7c6 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 13:33:45 -0600 Subject: [PATCH 8/9] Use double-ticks around parameter name in NEWS file. --- .../next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst b/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst index e5250a0d76dd30..fd5982d5df3648 100644 --- a/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst +++ b/Misc/NEWS.d/next/Library/2024-03-30-11-42-13.gh-issue-117391.u7KmA9.rst @@ -1,4 +1,4 @@ -Add `convert_keys` parameter to JSON encoding, to allow passing dict keys that +Add ``convert_keys`` parameter to JSON encoding, to allow passing dict keys that are not supported types in JSON through the encoder's :meth:`json.JSONEncoder.default()` method. Patch by Charles Cazabon. From f09af2e938f3d44c5ff5806282487c6c7adfc5d8 Mon Sep 17 00:00:00 2001 From: Charles Cazabon Date: Sat, 30 Mar 2024 17:50:16 -0600 Subject: [PATCH 9/9] Fix Python version of convert_key not always turning the result of passing the key through `.default()` to a str. --- Lib/json/encoder.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 240136518ee337..820dafc105b433 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -378,7 +378,23 @@ def _iterencode_dict(dct, _current_indent_level): key = _intstr(key) elif _convert_keys: key = _default(key) - if key is not None and not isinstance(key, (int, bool, float, str)): + 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) + else: raise TypeError(f'keys must be str, int, float, bool ' f'or None, not {key.__class__.__name__}') elif _skipkeys: