Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Lib/test/test_json/test_default.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``json.dump`` and ``json.dumps`` encoding certain dict subclasses as
empty.
5 changes: 4 additions & 1 deletion Modules/_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading