diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index d09a6c92bbd37d..8c390599d9812f 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -58,6 +58,10 @@ The class can be used to simulate nested scopes and is useful in templating. one of the underlying mappings gets updated, those changes will be reflected in :class:`ChainMap`. + .. versionchanged:: next + Added the :meth:`~object.__json__` method; + instances are now serializable by the :mod:`json` module. + All of the usual dictionary methods are supported. In addition, there is a *maps* attribute, a method for creating new subcontexts, and a property for accessing all but the first mapping: @@ -486,6 +490,10 @@ or subtracting from an empty counter. Deques are :ref:`generic ` over the type of their contents. + .. versionchanged:: next + Added the :meth:`~object.__json__` method; + instances are now serializable by the :mod:`json` module. + Deque objects support the following methods: @@ -1343,6 +1351,10 @@ attribute. :class:`!UserDict` instances. If arguments are provided, they are used to initialize :attr:`data`, like a regular dictionary. + .. versionchanged:: next + Added the :meth:`~object.__json__` method; + instances are now serializable by the :mod:`json` module. + In addition to supporting the methods and operations of mappings, :class:`!UserDict` instances provide the following attribute: @@ -1380,6 +1392,10 @@ to work with because the underlying list is accessible as an attribute. defaulting to the empty list ``[]``. *list* can be any iterable, for example a real Python list or a :class:`UserList` object. + .. versionchanged:: next + Added the :meth:`~object.__json__` method; + instances are now serializable by the :mod:`json` module. + In addition to supporting the methods and operations of mutable sequences, :class:`UserList` instances provide the following attribute: @@ -1429,3 +1445,7 @@ attribute. .. versionchanged:: 3.5 New methods ``__getnewargs__``, ``__rmod__``, ``casefold``, ``format_map``, ``isprintable``, and ``maketrans``. + + .. versionchanged:: next + Added the :meth:`~object.__json__` method; + instances are now serializable by the :mod:`json` module. diff --git a/Doc/library/copyreg.rst b/Doc/library/copyreg.rst index d59936029da69d..c14c89bb109ffc 100644 --- a/Doc/library/copyreg.rst +++ b/Doc/library/copyreg.rst @@ -1,21 +1,26 @@ -:mod:`!copyreg` --- Register :mod:`!pickle` support functions -============================================================= +.. _copyreg-register-pickle-support-functions: + +:mod:`!copyreg` --- Register :mod:`!pickle` and :mod:`!json` support functions +============================================================================== .. module:: copyreg - :synopsis: Register pickle support functions. + :synopsis: Register pickle and JSON support functions. **Source code:** :source:`Lib/copyreg.py` .. index:: pair: module; pickle pair: module; copy + pair: module; json -------------- -The :mod:`!copyreg` module offers a way to define functions used while pickling -specific objects. The :mod:`pickle` and :mod:`copy` modules use those functions -when pickling/copying those objects. The module provides configuration -information about object constructors which are not classes. +The :mod:`!copyreg` module offers a way to define functions +used while pickling and JSON-serializing specific objects. +The :mod:`pickle`, :mod:`copy` and :mod:`json` modules use those functions +when pickling/copying/serializing those objects. +The module provides configuration information +about object constructors which are not classes. Such constructors may be factory functions or class instances. @@ -39,6 +44,55 @@ Such constructors may be factory functions or class instances. object or subclass of :class:`pickle.Pickler` can also be used for declaring reduction functions. + +.. function:: json(type, function) + + Declares that *function* should be used + as the JSON serialization function for objects of type *type*. + *function* must be callable; + it is called with the object as its only argument + and must return a substitute object to be serialized, + with the same interface as the :meth:`~object.__json__` method. + Registration is by exact type: + it does not apply to subclasses of *type*. + See :ref:`json-protocol` for details. + + .. versionadded:: next + + +.. data:: json_dispatch_table + + The mapping of types to serialization functions + filled by :func:`json` and consulted by the :mod:`json` module. + It can be overridden for a particular encoder + with the :attr:`json.JSONEncoder.dispatch_table` attribute. + + .. versionadded:: next + + +.. class:: RawJSON(encoded_json) + + Wrapper for the already encoded JSON string *encoded_json*. + The JSON encoder outputs it verbatim, + without validation of its content. + ``str()`` of the instance returns *encoded_json*. + + For example, it allows serializing :class:`decimal.Decimal` + as a JSON number with full precision: + + >>> import copyreg, decimal, json + >>> copyreg.json(decimal.Decimal, lambda d: copyreg.RawJSON(str(d))) + >>> json.dumps({'price': decimal.Decimal('1.10')}) + '{"price": 1.10}' + + A registration can be undone by removing the entry from + :data:`json_dispatch_table`: + + >>> del copyreg.json_dispatch_table[decimal.Decimal] + + .. versionadded:: next + + Example ------- diff --git a/Doc/library/json.rst b/Doc/library/json.rst index 383ccad9df041b..0ab721488aa73e 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -243,6 +243,11 @@ Basic Usage .. versionchanged:: 3.6 All optional parameters are now :ref:`keyword-only `. + .. versionchanged:: next + Serialization can now be customized per type + with the :meth:`~object.__json__` method and :func:`copyreg.json`. + See :ref:`json-protocol`. + .. function:: dumps(obj, *, skipkeys=False, ensure_ascii=True, \ check_circular=True, allow_nan=True, cls=None, \ @@ -560,6 +565,14 @@ Encoders and Decoders .. versionchanged:: 3.6 All parameters are now :ref:`keyword-only `. + .. versionchanged:: next + The encoder now consults the dispatch table + (:attr:`~JSONEncoder.dispatch_table` + or :data:`copyreg.json_dispatch_table`) + and the :meth:`~object.__json__` method of the object's class + before falling back to *default*. + See :ref:`json-protocol`. + .. method:: default(o) @@ -598,6 +611,106 @@ Encoders and Decoders for chunk in json.JSONEncoder().iterencode(bigobject): mysocket.write(chunk) + .. attribute:: dispatch_table + + A mapping of types to serialization functions, + used instead of the global :data:`copyreg.json_dispatch_table`. + It can be set as a class attribute of a subclass + or as an attribute of an encoder instance. + See :ref:`json-protocol`. + + .. versionadded:: next + + +.. _json-protocol: + +Serializing custom objects +-------------------------- + +Serialization of objects of types that are not supported natively +(see the :ref:`conversion table `) +can be customized at three levels: + +* The author of a class can define the JSON representation of its instances + by giving it a :meth:`~object.__json__` method. + +* An application can register a serialization function + for instances of a type it does not control + with :func:`copyreg.json`. + +* A particular call can use its own type-to-function mapping + by setting the :attr:`~JSONEncoder.dispatch_table` attribute + of a :class:`JSONEncoder` subclass or instance. + +The more specific level takes precedence: +a registered function is used before ``__json__``, +and an encoder's own dispatch table completely replaces +the global :data:`copyreg.json_dispatch_table`. +The *default* function is called last, +only for objects that none of the above handled. + +.. currentmodule:: None + +.. method:: object.__json__() + + Return a substitute object to be serialized instead of *self*. + Called by the JSON encoder for an object + whose type is not supported natively + and has no entry in the dispatch table. + + If the result is of a serializable type, it is serialized as usual + (but a ``__json__`` method of the result is not consulted). + Otherwise the result is interpreted by duck typing: + + * an object with :meth:`~object.__index__` is serialized + as a JSON number; + * an object with :meth:`~object.__float__` is serialized + as a JSON number; + * an iterable with a :meth:`!keys` method and + :meth:`~object.__getitem__` is serialized as a JSON object; + * any other iterable is serialized as a JSON array; + * an object with :meth:`~object.__raw_json__` is included + in the output verbatim. + + .. versionadded:: next + +.. method:: object.__raw_json__() + + Return a string to be included in the JSON output verbatim, + without validation of its content. + Consulted only for the result of a registered serialization function + or a :meth:`~object.__json__` method. + To include an already encoded fragment directly in the serialized data, + wrap it in :class:`copyreg.RawJSON` instead. + + .. versionadded:: next + +.. currentmodule:: json + +For example, a class can serialize itself as a JSON object:: + + class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def __json__(self): + return {'x': self.x, 'y': self.y} + +An application can choose how :class:`decimal.Decimal` is serialized — +as a JSON string:: + + copyreg.json(decimal.Decimal, str) + +or as a JSON number with full precision, using :class:`copyreg.RawJSON`:: + + copyreg.json(decimal.Decimal, lambda d: copyreg.RawJSON(str(d))) + +A number of standard library container types define +:meth:`~object.__json__` and are therefore serializable out of the box. + +.. versionadded:: next + Exceptions ---------- diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 38a77119769d72..96f17fe6d58d8c 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -397,6 +397,10 @@ Standard names are defined for the following types: Updated to support the new union (``|``) operator from :pep:`584`, which simply delegates to the underlying mapping. + .. versionchanged:: next + Added the :meth:`~object.__json__` method; + instances are now serializable by the :mod:`json` module. + .. describe:: key in proxy Return ``True`` if the underlying mapping has a key *key*, else diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index da98f1ad6b9bc3..715ca273f0d4a2 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -96,6 +96,16 @@ codecs even when a built-in codec of the same name exists. (Contributed by Serhiy Storchaka in :gh:`152997`.) +copyreg +------- + +* Add :func:`copyreg.json`, :data:`copyreg.json_dispatch_table` + and :class:`copyreg.RawJSON` + for registering JSON serialization functions + for types that cannot be modified; + see the :mod:`json` entry below. + (Contributed by Serhiy Storchaka in :gh:`71549`.) + curses ------ @@ -293,6 +303,30 @@ Add :meth:`~ipaddress.IPv4Network.next_network` and network with a specific prefix size. +json +---- + +* JSON serialization of custom types can now be customized + without subclassing :class:`~json.JSONEncoder` or passing *default*: + a class can define its JSON representation + with the new :meth:`~object.__json__` method, + an application can register a serialization function + for a type it does not control with the new :func:`copyreg.json`, + and a particular encoder can use its own type-to-function mapping + via the new :attr:`~json.JSONEncoder.dispatch_table` attribute. + The new :class:`copyreg.RawJSON` wrapper includes + an already encoded JSON string in the output verbatim, + which allows, for example, serializing :class:`decimal.Decimal` + as a JSON number with full precision. + Standard library containers with an unambiguous JSON form + (:class:`collections.deque`, :class:`types.MappingProxyType`, + :class:`collections.ChainMap`, :class:`collections.UserDict`, + :class:`collections.UserList` and :class:`collections.UserString`) + are now serializable out of the box. + See :ref:`json-protocol`. + (Contributed by Serhiy Storchaka in :gh:`71549`.) + + logging ------- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 7d952a1e52561a..a188e8f4ddb9e1 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1446,6 +1446,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__iter__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__itruediv__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__ixor__)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__json__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__lazy_import__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__lazy_modules__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__le__)); @@ -1483,6 +1484,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__qualname__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__radd__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__rand__)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__raw_json__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__rdivmod__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__reduce__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__reduce_ex__)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 8a8bbc3b6d05bf..00d3895b2a878e 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -169,6 +169,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(__iter__) STRUCT_FOR_ID(__itruediv__) STRUCT_FOR_ID(__ixor__) + STRUCT_FOR_ID(__json__) STRUCT_FOR_ID(__lazy_import__) STRUCT_FOR_ID(__lazy_modules__) STRUCT_FOR_ID(__le__) @@ -206,6 +207,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(__qualname__) STRUCT_FOR_ID(__radd__) STRUCT_FOR_ID(__rand__) + STRUCT_FOR_ID(__raw_json__) STRUCT_FOR_ID(__rdivmod__) STRUCT_FOR_ID(__reduce__) STRUCT_FOR_ID(__reduce_ex__) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 366d2d300fb478..40eb8f20b2a165 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1444,6 +1444,7 @@ extern "C" { INIT_ID(__iter__), \ INIT_ID(__itruediv__), \ INIT_ID(__ixor__), \ + INIT_ID(__json__), \ INIT_ID(__lazy_import__), \ INIT_ID(__lazy_modules__), \ INIT_ID(__le__), \ @@ -1481,6 +1482,7 @@ extern "C" { INIT_ID(__qualname__), \ INIT_ID(__radd__), \ INIT_ID(__rand__), \ + INIT_ID(__raw_json__), \ INIT_ID(__rdivmod__), \ INIT_ID(__reduce__), \ INIT_ID(__reduce_ex__), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 00d6297432b5fc..135df232dbfe08 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -456,6 +456,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(__json__); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(__lazy_import__); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -604,6 +608,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(__raw_json__); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(__rdivmod__); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 20f1e728733fec..03041309d2be18 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -1164,6 +1164,9 @@ def __ror__(self, other): m.update(child) return self.__class__(m) + def __json__(self): + return self + ################################################################################ ### UserDict @@ -1274,6 +1277,9 @@ def fromkeys(cls, iterable, value=None): d[key] = value return d + def __json__(self): + return self.data + ################################################################################ ### UserList @@ -1407,6 +1413,9 @@ def extend(self, other): else: self.data.extend(other) + def __json__(self): + return self.data + ################################################################################ ### UserString @@ -1662,3 +1671,6 @@ def upper(self): def zfill(self, width): return self.__class__(self.data.zfill(width)) + + def __json__(self): + return self.data diff --git a/Lib/copyreg.py b/Lib/copyreg.py index a5e8add4a554d7..d54e704f0f3c3f 100644 --- a/Lib/copyreg.py +++ b/Lib/copyreg.py @@ -1,10 +1,11 @@ -"""Helper to provide extensibility for pickle. +"""Helper to provide extensibility for pickle and JSON. -This is only useful to add pickle support for extension types defined in -C, not for instances of user-defined classes. +This is only useful to add support for types that cannot be modified, +such as extension types defined in C, not for instances of user-defined +classes. """ -__all__ = ["pickle", "constructor", +__all__ = ["pickle", "constructor", "json", "RawJSON", "add_extension", "remove_extension", "clear_extension_cache"] dispatch_table = {} @@ -23,6 +24,34 @@ def constructor(object): if not callable(object): raise TypeError("constructors must be callable") +# Support for JSON serialization + +json_dispatch_table = {} + +def json(ob_type, json_function): + if not callable(json_function): + raise TypeError("json functions must be callable") + json_dispatch_table[ob_type] = json_function + + +class RawJSON: + """Wrapper for already encoded JSON strings. + + It is serialized by the JSON encoder as is. + """ + + def __init__(self, encoded_json): + self._encoded_json = encoded_json + + def __json__(self): + return self + + def __raw_json__(self): + return self._encoded_json + + def __str__(self): + return self._encoded_json + # Example: provide pickling support for complex numbers. def pickle_complex(c): diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 718b3254241c56..153f0963811417 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -1,6 +1,8 @@ """Implementation of JSONEncoder """ +import operator import re +from copyreg import json_dispatch_table try: from _json import encode_basestring_ascii as c_encode_basestring_ascii @@ -254,70 +256,70 @@ def floatstr(o, allow_nan=self.allow_nan, _iterencode = c_make_encoder( markers, self.default, _encoder, indent, self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, self.allow_nan) + self.skipkeys, self.allow_nan, + getattr(self, 'dispatch_table', json_dispatch_table) + ) else: _iterencode = _make_iterencode( markers, self.default, _encoder, indent, floatstr, self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot) + self.skipkeys, _one_shot, + getattr(self, 'dispatch_table', json_dispatch_table)) return _iterencode(o, 0) def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + dispatch_table, ): + _simple_encoders = { + str: _encoder, + type(None): lambda o: 'null', + bool: lambda o: 'true' if o else 'false', + # Subclasses of int/float may override __repr__, but we still + # want to encode them as integers/floats in JSON. One example + # within the standard library is IntEnum. They are handled in + # _iterencode(). + int: int.__repr__, + float: _floatstr, + } + def _iterencode_list(lst, _current_indent_level): - if not lst: - yield '[]' - return - if markers is not None: - markerid = id(lst) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = lst - buf = '[' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + _indent * _current_indent_level - separator = _item_separator + newline_indent - buf += newline_indent - else: - newline_indent = None - separator = _item_separator + first = True for i, value in enumerate(lst): - if i: + if first: + first = False + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + _indent * _current_indent_level + separator = _item_separator + newline_indent + buf += newline_indent + else: + newline_indent = None + separator = _item_separator + else: buf = separator try: - if isinstance(value, str): - yield buf + _encoder(value) - elif value is None: - yield buf + 'null' - elif value is True: - yield buf + 'true' - elif value is False: - yield buf + 'false' - elif isinstance(value, int): - # Subclasses of int/float may override __repr__, but we still - # want to encode them as integers/floats in JSON. One example - # within the standard library is IntEnum. - yield buf + int.__repr__(value) - elif isinstance(value, float): - # see comment above for int - yield buf + _floatstr(value) + encoder = _simple_encoders.get(type(value)) + if encoder is not None: + yield buf + encoder(value) else: yield buf - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, (dict, frozendict)): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - yield from chunks + yield from _iterencode(value, _current_indent_level) except GeneratorExit: raise except BaseException as exc: exc.add_note(f'when serializing {type(lst).__name__} item {i}') raise + if first: + yield '[]' + return if newline_indent is not None: _current_indent_level -= 1 yield '\n' + _indent * _current_indent_level @@ -326,28 +328,13 @@ def _iterencode_list(lst, _current_indent_level): del markers[markerid] def _iterencode_dict(dct, _current_indent_level): - if not dct: - yield '{}' - return - if markers is not None: - markerid = id(dct) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = dct - yield '{' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + _indent * _current_indent_level - item_separator = _item_separator + newline_indent - else: - newline_indent = None - item_separator = _item_separator first = True if _sort_keys: - items = sorted(dct.items()) + keys = sorted(dct) else: - items = dct.items() - for key, value in items: + keys = dct + for key in keys: + value = dct[key] if isinstance(key, str): pass # JavaScript is weakly typed for these, so it makes sense to @@ -371,56 +358,78 @@ def _iterencode_dict(dct, _current_indent_level): f'not {key.__class__.__name__}') if first: first = False - if newline_indent is not None: + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + _indent * _current_indent_level + item_separator = _item_separator + newline_indent yield newline_indent + else: + newline_indent = None + item_separator = _item_separator else: yield item_separator yield _encoder(key) yield _key_separator try: - if isinstance(value, str): - yield _encoder(value) - elif value is None: - yield 'null' - elif value is True: - yield 'true' - elif value is False: - yield 'false' - elif isinstance(value, int): - # see comment for int/float in _make_iterencode - yield int.__repr__(value) - elif isinstance(value, float): - # see comment for int/float in _make_iterencode - yield _floatstr(value) + encoder = _simple_encoders.get(type(value)) + if encoder is not None: + yield encoder(value) else: - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, (dict, frozendict)): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - yield from chunks + yield from _iterencode(value, _current_indent_level) except GeneratorExit: raise except BaseException as exc: exc.add_note(f'when serializing {type(dct).__name__} item {key!r}') raise - if not first and newline_indent is not None: + if first: + yield '{}' + return + if newline_indent is not None: _current_indent_level -= 1 yield '\n' + _indent * _current_indent_level yield '}' if markers is not None: del markers[markerid] + # The branch order in _iterencode() is a performance invariant: + # exact basic types are encoded without any attribute lookup; the + # dispatch table and __json__ are consulted before the isinstance() + # fallbacks, solely so that subclasses of basic types can customize + # their serialization; duck typing of a hook result (including + # __raw_json__) applies only if a hook has fired, so that objects + # bound for default() pay no extra lookups. def _iterencode(o, _current_indent_level): + cls = type(o) + encoder = _simple_encoders.get(cls) + if encoder is not None: + yield encoder(o) + return + if cls is list or cls is tuple: + yield from _iterencode_list(o, _current_indent_level) + return + if cls is dict or cls is frozendict: + yield from _iterencode_dict(o, _current_indent_level) + return + + asjson = dispatch_table.get(cls) + if asjson is None: + asjson = getattr(cls, '__json__', None) + if asjson is not None: + o = asjson(o) + cls = type(o) + encoder = _simple_encoders.get(cls) + if encoder is not None: + yield encoder(o) + return + if isinstance(o, str): yield _encoder(o) - elif o is None: - yield 'null' - elif o is True: - yield 'true' - elif o is False: - yield 'false' elif isinstance(o, int): # see comment for int/float in _make_iterencode yield int.__repr__(o) @@ -431,6 +440,29 @@ def _iterencode(o, _current_indent_level): yield from _iterencode_list(o, _current_indent_level) elif isinstance(o, (dict, frozendict)): yield from _iterencode_dict(o, _current_indent_level) + elif asjson is not None: + # The result of __json__() or a dispatch table function is + # not directly serializable. Interpret it by duck typing. + if getattr(cls, '__index__', None) is not None: + yield int.__repr__(operator.index(o)) + elif getattr(cls, '__float__', None) is not None: + yield _floatstr(float(o)) + elif getattr(cls, '__iter__', None) is not None: + if (getattr(cls, '__getitem__', None) is not None + and hasattr(cls, 'keys')): + yield from _iterencode_dict(o, _current_indent_level) + else: + yield from _iterencode_list(o, _current_indent_level) + else: + asrawjson = getattr(cls, '__raw_json__', None) + if asrawjson is None: + raise TypeError(f'Object of type {cls.__name__} ' + f'is not JSON serializable') + raw = asrawjson(o) + if not isinstance(raw, str): + raise TypeError(f'__raw_json__() must return a string, ' + f'not {raw.__class__.__name__}') + yield raw else: if markers is not None: markerid = id(o) diff --git a/Lib/test/test_copyreg.py b/Lib/test/test_copyreg.py index e158c19db2d65a..5dc21fa303ab93 100644 --- a/Lib/test/test_copyreg.py +++ b/Lib/test/test_copyreg.py @@ -49,6 +49,23 @@ def test_bool(self): import copy self.assertEqual(True, copy.copy(True)) + def test_json(self): + copyreg.json(C, str) + try: + self.assertIs(copyreg.json_dispatch_table[C], str) + finally: + del copyreg.json_dispatch_table[C] + + def test_noncallable_json(self): + self.assertRaises(TypeError, copyreg.json, + C, "not a callable") + + def test_raw_json(self): + raw = copyreg.RawJSON('[1, 2]') + self.assertEqual(str(raw), '[1, 2]') + self.assertEqual(raw.__raw_json__(), '[1, 2]') + self.assertIs(raw.__json__(), raw) + def test_extension_registry(self): mod, func, code = 'junk1 ', ' junk2', 0xabcd e = ExtensionSaver(code) diff --git a/Lib/test/test_json/test_default.py b/Lib/test/test_json/test_default.py index 811880a15c8020..c523a66975f3f4 100644 --- a/Lib/test/test_json/test_default.py +++ b/Lib/test/test_json/test_default.py @@ -1,6 +1,41 @@ import collections +import copyreg +import decimal +import types from test.test_json import PyTest, CTest +class IndexLike: + def __init__(self, value): + self.value = value + def __index__(self): + return self.value + +class FloatLike: + def __init__(self, value): + self.value = value + def __float__(self): + return self.value + +class MappingLike: + def __init__(self, dict): + self.dict = dict + def __len__(self): + return len(self.dict) + def __iter__(self): + return iter(self.dict) + def __getitem__(self, key): + return self.dict[key] + def keys(self): + yield from self.dict.keys() + def items(self): + yield from self.dict.items() + +class MyRawJSON: + def __init__(self, value): + self.value = value + def __raw_json__(self): + return self.value + class TestDefault: def test_default(self): @@ -36,6 +71,143 @@ def test_ordereddict(self): self.dumps(od, sort_keys=True), '{"a": 1, "b": 2, "c": 3, "d": 4}') + def test_deque(self): + self.assertEqual( + self.dumps(collections.deque([1, 2, 3])), + '[1, 2, 3]') + self.assertEqual( + self.dumps(collections.deque([1, 2, 3], maxlen=2)), + '[2, 3]') + + def test_mappingproxy(self): + self.assertEqual( + self.dumps(types.MappingProxyType(collections.OrderedDict(a=1, b=2))), + '{"a": 1, "b": 2}') + + def test_chainmap(self): + cm = collections.ChainMap({'a': 1, 'b': 2}, {'a': 3, 'c': 4}) + self.assertEqual( + self.dumps(cm), + '{"a": 1, "c": 4, "b": 2}') + self.assertEqual( + self.dumps(cm, sort_keys=True), + '{"a": 1, "b": 2, "c": 4}') + cm = collections.ChainMap({'a': 1, 'b': 2}, {'a': 3, 'c': 4}) + self.assertEqual( + self.dumps(cm), + '{"a": 1, "c": 4, "b": 2}') + self.assertEqual( + self.dumps(cm, sort_keys=True), + '{"a": 1, "b": 2, "c": 4}') + self.assertEqual( + self.dumps(collections.ChainMap({}, {'a': 2})), + '{"a": 2}') + + def test_userdict(self): + ud = collections.UserDict({'b': 1, 'a': 2}) + self.assertEqual( + self.dumps(ud), + '{"b": 1, "a": 2}') + self.assertEqual( + self.dumps(ud, sort_keys=True), + '{"a": 2, "b": 1}') + ud = collections.UserDict(MappingLike({'b': 1, 'a': 2})) + self.assertEqual( + self.dumps(ud), + '{"b": 1, "a": 2}') + self.assertEqual( + self.dumps(ud, sort_keys=True), + '{"a": 2, "b": 1}') + + def test_userlist(self): + self.assertEqual( + self.dumps(collections.UserList([1, 2, 3])), + '[1, 2, 3]') + + self.assertEqual( + self.dumps(collections.UserList(collections.deque([1, 2, 3]))), + '[1, 2, 3]') + + def test_userstring(self): + self.assertEqual( + self.dumps(collections.UserString('abc')), + '"abc"') + + def test_copyreg_json(self): + class A: pass + a = A() + self.assertRaises(TypeError, self.dumps, a) + copyreg.json(A, repr) + try: + self.assertEqual(self.dumps(a), self.dumps(repr(a))) + finally: + del copyreg.json_dispatch_table[A] + self.assertRaises(TypeError, self.dumps, a) + + def test_encoder_dispatch_table(self): + class A: pass + class Encoder(self.json.JSONEncoder): + dispatch_table = {A: repr} + a = A() + encoder = Encoder() + self.assertEqual(encoder.encode(a), encoder.encode(repr(a))) + + def test_json_method(self): + for result in (None, True, False, 42, 1.25, 'string', [1, 2], {'a': 'b'}): + with self.subTest(obj=result): + class A: + def __json__(self): + return result + a = A() + self.assertEqual(self.dumps(a), self.dumps(result)) + for result, expected in [ + (IndexLike(42), '42'), + (FloatLike(1.25), '1.25'), + (iter([]), '[]'), + (iter([1, 2]), '[1, 2]'), + (MappingLike({}), '{}'), + (MappingLike({'a': 1, 'b': 2}), '{"a": 1, "b": 2}'), + (MyRawJSON('123.00'), '123.00'), + ]: + with self.subTest(obj=result): + class A: + def __json__(self): + return result + a = A() + self.assertEqual(self.dumps(a), expected) + + def test_bad_raw_json(self): + class A: + def __json__(self): + return MyRawJSON(42) + with self.assertRaisesRegex(TypeError, + r'__raw_json__\(\) must return a string, not int'): + self.dumps(A()) + + def test_decimal(self): + d = decimal.Decimal('0.12345678901234567890') + self.assertRaises(TypeError, self.dumps, d) + copyreg.json(decimal.Decimal, str) + try: + self.assertEqual(self.dumps(d), '"0.12345678901234567890"') + + copyreg.json(decimal.Decimal, + lambda obj: copyreg.RawJSON(str(obj))) + self.assertEqual(self.dumps(d), '0.12345678901234567890') + finally: + del copyreg.json_dispatch_table[decimal.Decimal] + + def test_namedtuple(self): + A = collections.namedtuple('A', ('name', 'age')) + a = A('Alice', '25') + self.assertEqual(self.dumps(a), '["Alice", "25"]') + copyreg.json(A, lambda obj: obj._asdict()) + try: + self.assertEqual(self.dumps(a), '{"name": "Alice", "age": "25"}') + self.assertEqual(self.dumps(a, sort_keys=True), '{"age": "25", "name": "Alice"}') + finally: + del copyreg.json_dispatch_table[A] + class TestPyDefault(TestDefault, PyTest): pass class TestCDefault(TestDefault, CTest): pass diff --git a/Lib/test/test_json/test_speedups.py b/Lib/test/test_json/test_speedups.py index 0b22a0bf4b9538..f70d54b7ffbd47 100644 --- a/Lib/test/test_json/test_speedups.py +++ b/Lib/test/test_json/test_speedups.py @@ -46,7 +46,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, {}) with self.assertRaises(TypeError): enc('spam', 4) with self.assertRaises(TypeError): @@ -56,7 +56,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, {}) with self.assertRaises(ZeroDivisionError): enc('spam', 4) @@ -67,7 +67,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, {}) def test_bad_bool_args(self): def test(name): @@ -92,7 +92,8 @@ def test_current_indent_level(self): item_separator=', ', sort_keys=False, skipkeys=False, - allow_nan=False) + allow_nan=False, + dispatch_table={}) expected = ( '[\n' '\t"spam", \n' @@ -139,7 +140,7 @@ def encode_str(obj): None, lambda o: "null", encode_str, None, ": ", ", ", False, - False, True + False, True, {} ) # Must not crash (use-after-free under ASan before fix) @@ -164,7 +165,7 @@ def default(obj): None, default, self.json.encoder.c_encode_basestring, None, ": ", ", ", False, - False, True + False, True, {} ) # Must not crash (use-after-free under ASan before fix) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 2084b30d71ff6c..c652624cdf58fb 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -1203,6 +1203,7 @@ def test_methods(self): '__class_getitem__', '__ior__', '__iter__', + '__json__', '__len__', '__or__', '__reversed__', diff --git a/Misc/NEWS.d/next/Library/2026-07-12-10-30-00.gh-issue-71549.cWr8Kd.rst b/Misc/NEWS.d/next/Library/2026-07-12-10-30-00.gh-issue-71549.cWr8Kd.rst new file mode 100644 index 00000000000000..4977a2037ca01a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-12-10-30-00.gh-issue-71549.cWr8Kd.rst @@ -0,0 +1,14 @@ +Add support for JSON serialization of custom types. +A class can define the JSON representation of its instances +with the new :meth:`~object.__json__` and :meth:`~object.__raw_json__` +methods. +A serialization function for instances of a specific type +can be registered with the new :func:`copyreg.json` +and overridden per encoder +with the new :attr:`json.JSONEncoder.dispatch_table` attribute. +The new :class:`copyreg.RawJSON` wrapper includes +an already encoded JSON string in the serialized data verbatim. +:class:`collections.deque`, :class:`types.MappingProxyType`, +:class:`collections.ChainMap`, :class:`collections.UserDict`, +:class:`collections.UserList` and :class:`collections.UserString` +are now serializable out of the box. diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 62d1dad5735ec8..8f60bb8a040e82 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -1799,6 +1799,12 @@ deque___sizeof___impl(dequeobject *deque) return PyLong_FromSize_t(res); } +static PyObject * +deque_json(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return Py_NewRef(self); +} + static PyObject * deque_get_maxlen(PyObject *self, void *Py_UNUSED(closure)) { @@ -1856,6 +1862,8 @@ static PyMethodDef deque_methods[] = { DEQUE___SIZEOF___METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("deques are generic over the type of their contents")}, + {"__json__", deque_json, + METH_NOARGS, NULL}, {NULL, NULL} /* sentinel */ }; diff --git a/Modules/_json.c b/Modules/_json.c index 3a724a3e72b185..029fb6cf2a4e50 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -60,6 +60,7 @@ typedef struct _PyEncoderObject { PyObject *indent; PyObject *key_separator; PyObject *item_separator; + PyObject *dispatch_table; char sort_keys; char skipkeys; int allow_nan; @@ -1309,17 +1310,18 @@ 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", "dispatch_table", NULL}; PyEncoderObject *s; PyObject *markers, *defaultfn, *encoder, *indent, *key_separator; - PyObject *item_separator; + PyObject *item_separator, *dispatch_table; int sort_keys, skipkeys, allow_nan; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppp:make_encoder", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUpppO!:make_encoder", kwlist, &markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator, - &sort_keys, &skipkeys, &allow_nan)) + &sort_keys, &skipkeys, &allow_nan, + &PyDict_Type, &dispatch_table)) return NULL; if (markers != Py_None && !PyDict_Check(markers)) { @@ -1339,6 +1341,7 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) s->indent = Py_NewRef(indent); s->key_separator = Py_NewRef(key_separator); s->item_separator = Py_NewRef(item_separator); + s->dispatch_table = Py_NewRef(dispatch_table); s->sort_keys = sort_keys; s->skipkeys = skipkeys; s->allow_nan = allow_nan; @@ -1569,7 +1572,15 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer, PyObject *obj, Py_ssize_t indent_level, PyObject *indent_cache) { - /* Encode Python object obj to a JSON term */ + /* Encode Python object obj to a JSON term. + + The branch order is a performance invariant: exact basic types + are encoded without any attribute lookup; the dispatch table and + __json__ are consulted before the subtype checks, solely so that + subclasses of basic types can customize their serialization; + duck typing of a hook result (including __raw_json__) applies + only if a hook has fired, so that objects bound for the default + function pay no extra lookups. */ PyObject *newobj; int rv; @@ -1582,87 +1593,235 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer, else if (obj == Py_False) { return PyUnicodeWriter_WriteASCII(writer, "false", 5); } - else if (PyUnicode_Check(obj)) { + else if (PyUnicode_CheckExact(obj)) { return encoder_write_string(s, writer, obj); } - else if (PyLong_Check(obj)) { - if (PyLong_CheckExact(obj)) { - // Fast-path for exact integers - return PyUnicodeWriter_WriteRepr(writer, obj); + else if (PyLong_CheckExact(obj)) { + // Fast-path for exact integers + return PyUnicodeWriter_WriteRepr(writer, obj); + } + else if (PyFloat_CheckExact(obj)) { + PyObject *encoded = encoder_encode_float(s, obj); + if (encoded == NULL) + return -1; + return _steal_accumulate(writer, encoded); + } + else if (PyList_CheckExact(obj) || PyTuple_CheckExact(obj)) { + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) + return -1; + rv = encoder_listencode_list(s, writer, obj, indent_level, indent_cache); + _Py_LeaveRecursiveCall(); + return rv; + } + else if (PyAnyDict_CheckExact(obj)) { + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) + return -1; + rv = encoder_listencode_dict(s, writer, obj, indent_level, indent_cache); + _Py_LeaveRecursiveCall(); + return rv; + } + + PyObject *asjson; + if (PyDict_GetItemRef(s->dispatch_table, (PyObject *)Py_TYPE(obj), + &asjson) < 0) + { + return -1; + } + if (asjson == NULL + && PyObject_GetOptionalAttr((PyObject *)Py_TYPE(obj), + &_Py_ID(__json__), &asjson) < 0) + { + return -1; + } + newobj = NULL; + if (asjson != NULL) { + obj = newobj = PyObject_CallOneArg(asjson, obj); + Py_DECREF(asjson); + if (newobj == NULL) { + return -1; + } + if (obj == Py_None) { + Py_DECREF(newobj); + return PyUnicodeWriter_WriteASCII(writer, "null", 4); } + else if (obj == Py_True) { + Py_DECREF(newobj); + return PyUnicodeWriter_WriteASCII(writer, "true", 4); + } + else if (obj == Py_False) { + Py_DECREF(newobj); + return PyUnicodeWriter_WriteASCII(writer, "false", 5); + } + } + + if (PyUnicode_Check(obj)) { + rv = encoder_write_string(s, writer, obj); + Py_XDECREF(newobj); + return rv; + } + else if (PyLong_Check(obj)) { PyObject *encoded = PyLong_Type.tp_repr(obj); + Py_XDECREF(newobj); if (encoded == NULL) return -1; return _steal_accumulate(writer, encoded); } else if (PyFloat_Check(obj)) { PyObject *encoded = encoder_encode_float(s, obj); + Py_XDECREF(newobj); if (encoded == NULL) return -1; return _steal_accumulate(writer, encoded); } else if (PyList_Check(obj) || PyTuple_Check(obj)) { - if (_Py_EnterRecursiveCall(" while encoding a JSON object")) + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) { + Py_XDECREF(newobj); return -1; + } rv = encoder_listencode_list(s, writer, obj, indent_level, indent_cache); _Py_LeaveRecursiveCall(); + Py_XDECREF(newobj); return rv; } else if (PyAnyDict_Check(obj)) { - if (_Py_EnterRecursiveCall(" while encoding a JSON object")) + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) { + Py_XDECREF(newobj); return -1; + } rv = encoder_listencode_dict(s, writer, obj, indent_level, indent_cache); _Py_LeaveRecursiveCall(); + Py_XDECREF(newobj); return rv; } - else { - PyObject *ident = NULL; - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(obj); - if (ident == NULL) + else if (newobj != NULL) { + /* The result of __json__() or a dispatch table function is not + directly serializable. Interpret it by duck typing. */ + if (PyIndex_Check(obj)) { + PyObject *tmp = PyNumber_Index(obj); + Py_DECREF(newobj); + if (tmp == NULL) { + return -1; + } + PyObject *encoded = PyLong_Type.tp_repr(tmp); + Py_DECREF(tmp); + if (encoded == NULL) return -1; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - Py_DECREF(ident); + return _steal_accumulate(writer, encoded); + } + if (Py_TYPE(obj)->tp_as_number != NULL + && Py_TYPE(obj)->tp_as_number->nb_float != NULL) + { + PyObject *tmp = PyNumber_Float(obj); + Py_DECREF(newobj); + if (tmp == NULL) { return -1; } - if (PyDict_SetItem(s->markers, ident, obj)) { - Py_DECREF(ident); + PyObject *encoded = encoder_encode_float(s, tmp); + Py_DECREF(tmp); + if (encoded == NULL) return -1; + return _steal_accumulate(writer, encoded); + } + if (Py_TYPE(obj)->tp_iter != NULL) { + PyObject *func; + if (PyObject_GetOptionalAttr(obj, &_Py_ID(keys), &func) < 0) { + Py_DECREF(newobj); + return -1; + } + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) { + Py_XDECREF(func); + Py_DECREF(newobj); + return -1; + } + if (func != NULL) { + Py_DECREF(func); + rv = encoder_listencode_dict(s, writer, obj, indent_level, + indent_cache); + } + else { + rv = encoder_listencode_list(s, writer, obj, indent_level, + indent_cache); } + _Py_LeaveRecursiveCall(); + Py_DECREF(newobj); + return rv; } - newobj = PyObject_CallOneArg(s->defaultfn, obj); - if (newobj == NULL) { - Py_XDECREF(ident); + PyObject *asrawjson; + if (PyObject_GetOptionalAttr((PyObject *)Py_TYPE(obj), + &_Py_ID(__raw_json__), &asrawjson) < 0) + { + Py_DECREF(newobj); return -1; } - - if (_Py_EnterRecursiveCall(" while encoding a JSON object")) { + if (asrawjson != NULL) { + PyObject *encoded = PyObject_CallOneArg(asrawjson, obj); + Py_DECREF(asrawjson); Py_DECREF(newobj); - Py_XDECREF(ident); + if (encoded == NULL) { + return -1; + } + if (!PyUnicode_Check(encoded)) { + PyErr_Format(PyExc_TypeError, + "__raw_json__() must return a string, not %.80s", + Py_TYPE(encoded)->tp_name); + Py_DECREF(encoded); + return -1; + } + return _steal_accumulate(writer, encoded); + } + PyErr_Format(PyExc_TypeError, + "Object of type %.100s is not JSON serializable", + Py_TYPE(obj)->tp_name); + Py_DECREF(newobj); + return -1; + } + + PyObject *ident = NULL; + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(obj); + if (ident == NULL) + return -1; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + Py_DECREF(ident); return -1; } - rv = encoder_listencode_obj(s, writer, newobj, indent_level, indent_cache); - _Py_LeaveRecursiveCall(); + if (PyDict_SetItem(s->markers, ident, obj)) { + Py_DECREF(ident); + return -1; + } + } + newobj = PyObject_CallOneArg(s->defaultfn, obj); + if (newobj == NULL) { + Py_XDECREF(ident); + return -1; + } + if (_Py_EnterRecursiveCall(" while encoding a JSON object")) { Py_DECREF(newobj); - if (rv) { - _PyErr_FormatNote("when serializing %T object", obj); + Py_XDECREF(ident); + return -1; + } + rv = encoder_listencode_obj(s, writer, newobj, indent_level, indent_cache); + _Py_LeaveRecursiveCall(); + + Py_DECREF(newobj); + if (rv) { + _PyErr_FormatNote("when serializing %T object", obj); + Py_XDECREF(ident); + return -1; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) { Py_XDECREF(ident); return -1; } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) { - Py_XDECREF(ident); - return -1; - } - Py_XDECREF(ident); - } - return rv; + Py_XDECREF(ident); } + return rv; } static int @@ -1802,7 +1961,7 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer, PyObject *ident = NULL; bool first = true; - if (PyDict_GET_SIZE(dct) == 0) { + if (PyAnyDict_Check(dct) && PyDict_GET_SIZE(dct) == 0) { /* Fast path */ return PyUnicodeWriter_WriteASCII(writer, "{}", 2); } @@ -2012,6 +2171,7 @@ encoder_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(self->indent); Py_VISIT(self->key_separator); Py_VISIT(self->item_separator); + Py_VISIT(self->dispatch_table); return 0; } @@ -2026,10 +2186,11 @@ encoder_clear(PyObject *op) Py_CLEAR(self->indent); Py_CLEAR(self->key_separator); Py_CLEAR(self->item_separator); + Py_CLEAR(self->dispatch_table); return 0; } -PyDoc_STRVAR(encoder_doc, "Encoder(markers, default, encoder, indent, key_separator, item_separator, sort_keys, skipkeys, allow_nan)"); +PyDoc_STRVAR(encoder_doc, "Encoder(markers, default, encoder, indent, key_separator, item_separator, sort_keys, skipkeys, allow_nan, dispatch_table)"); static PyType_Slot PyEncoderType_slots[] = { {Py_tp_doc, (void *)encoder_doc}, diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 1c068e96f24532..96a75037129658 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1155,6 +1155,12 @@ mappingproxy_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) return PyObject_CallMethodNoArgs(pp->mapping, &_Py_ID(copy)); } +static PyObject * +mappingproxy_json(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return Py_NewRef(self); +} + static PyObject * mappingproxy_reversed(PyObject *self, PyObject *Py_UNUSED(ignored)) { @@ -1181,6 +1187,8 @@ static PyMethodDef mappingproxy_methods[] = { PyDoc_STR("mappingproxy objects are generic over two types, signifying (respectively) the types of their keys and values")}, {"__reversed__", mappingproxy_reversed, METH_NOARGS, PyDoc_STR("D.__reversed__() -> reverse iterator")}, + {"__json__", mappingproxy_json, METH_NOARGS, + NULL}, {0} };