diff --git a/Lib/test/test_json/test_default.py b/Lib/test/test_json/test_default.py index 811880a15c8020..7f3ef8b8cb9f96 100644 --- a/Lib/test/test_json/test_default.py +++ b/Lib/test/test_json/test_default.py @@ -1,7 +1,29 @@ import collections +import collections.abc from test.test_json import PyTest, CTest +class CustomIndexDict(collections.abc.Mapping, dict): + # Using `Mapping` here to make this a complete dict subclass, but note the bug in #110941 + # is not specific to `Mapping` subclasses and the outcome would have been the same with a + # fully manually implemented dict subclass. + + def __init__(self, keys: tuple = ()): + self._keys = keys + + def __getitem__(self, k): + try: + return self._keys.index(k) + except ValueError: + raise KeyError(k) + + def __iter__(self): + return iter(self._keys) + + def __len__(self): + return len(self._keys) + + class TestDefault: def test_default(self): self.assertEqual( @@ -36,6 +58,23 @@ def test_ordereddict(self): self.dumps(od, sort_keys=True), '{"a": 1, "b": 2, "c": 3, "d": 4}') + # This should behave identically for PyTest and CTest: see #110941. + def test_custom_dict(self): + cd = CustomIndexDict(("b", "a")) + self.assertEqual( + self.dumps(cd), + '{"b": 0, "a": 1}') + self.assertEqual( + self.dumps(cd, sort_keys=True), + '{"a": 1, "b": 0}') + + def test_empty_custom_dict(self): + # Exercise the fast path when a dict subclass is empty. + cd = CustomIndexDict() + self.assertEqual( + self.dumps(cd), + '{}') + class TestPyDefault(TestDefault, PyTest): pass class TestCDefault(TestDefault, CTest): pass diff --git a/Misc/NEWS.d/next/Library/2023-10-18-13-31-10.gh-issue-110941.UVrYGE.rst b/Misc/NEWS.d/next/Library/2023-10-18-13-31-10.gh-issue-110941.UVrYGE.rst new file mode 100644 index 00000000000000..5824b593460e9b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-18-13-31-10.gh-issue-110941.UVrYGE.rst @@ -0,0 +1,2 @@ +Fix ``json.dump`` and ``json.dumps`` encoding certain dict subclasses as +empty. diff --git a/Modules/_json.c b/Modules/_json.c index 6c4f38834631d3..cab80f7bebbce0 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1803,7 +1803,10 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer, PyObject *ident = NULL; bool first = true; - if (PyDict_GET_SIZE(dct) == 0) { + // Only take the fast path for exact dicts: a subclass may keep its + // contents outside the dict storage, so PyDict_GET_SIZE could be 0 while + // the mapping is non-empty (gh-110941). + if (PyAnyDict_CheckExact(dct) && PyDict_GET_SIZE(dct) == 0) { /* Fast path */ return PyUnicodeWriter_WriteASCII(writer, "{}", 2); }