Skip to content

Commit ca0cc50

Browse files
gh-110941: Fix json.dump encoding dict subclasses as empty (continued) (#150631)
* gh-110941: Fix `json.dump` encoding dict subclasses as empty. Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com> Co-authored-by: Alexander Ljungberg <aljungberg@wireload.net>
1 parent f871449 commit ca0cc50

3 files changed

Lines changed: 45 additions & 1 deletion

File tree

‎Lib/test/test_json/test_default.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
import collections
2+
import collections.abc
23
from test.test_json import PyTest, CTest
34

45

6+
class CustomIndexDict(collections.abc.Mapping, dict):
7+
# Using `Mapping` here to make this a complete dict subclass, but note the bug in #110941
8+
# is not specific to `Mapping` subclasses and the outcome would have been the same with a
9+
# fully manually implemented dict subclass.
10+
11+
def __init__(self, keys: tuple = ()):
12+
self._keys = keys
13+
14+
def __getitem__(self, k):
15+
try:
16+
return self._keys.index(k)
17+
except ValueError:
18+
raise KeyError(k)
19+
20+
def __iter__(self):
21+
return iter(self._keys)
22+
23+
def __len__(self):
24+
return len(self._keys)
25+
26+
527
class TestDefault:
628
def test_default(self):
729
self.assertEqual(
@@ -36,6 +58,23 @@ def test_ordereddict(self):
3658
self.dumps(od, sort_keys=True),
3759
'{"a": 1, "b": 2, "c": 3, "d": 4}')
3860

61+
# This should behave identically for PyTest and CTest: see #110941.
62+
def test_custom_dict(self):
63+
cd = CustomIndexDict(("b", "a"))
64+
self.assertEqual(
65+
self.dumps(cd),
66+
'{"b": 0, "a": 1}')
67+
self.assertEqual(
68+
self.dumps(cd, sort_keys=True),
69+
'{"a": 1, "b": 0}')
70+
71+
def test_empty_custom_dict(self):
72+
# Exercise the fast path when a dict subclass is empty.
73+
cd = CustomIndexDict()
74+
self.assertEqual(
75+
self.dumps(cd),
76+
'{}')
77+
3978

4079
class TestPyDefault(TestDefault, PyTest): pass
4180
class TestCDefault(TestDefault, CTest): pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix ``json.dump`` and ``json.dumps`` encoding certain dict subclasses as
2+
empty.

‎Modules/_json.c‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1802,7 +1802,10 @@ encoder_listencode_dict(PyEncoderObject *s, PyUnicodeWriter *writer,
18021802
PyObject *ident = NULL;
18031803
bool first = true;
18041804

1805-
if (PyDict_GET_SIZE(dct) == 0) {
1805+
// Only take the fast path for exact dicts: a subclass may keep its
1806+
// contents outside the dict storage, so PyDict_GET_SIZE could be 0 while
1807+
// the mapping is non-empty (gh-110941).
1808+
if (PyAnyDict_CheckExact(dct) && PyDict_GET_SIZE(dct) == 0) {
18061809
/* Fast path */
18071810
return PyUnicodeWriter_WriteASCII(writer, "{}", 2);
18081811
}

0 commit comments

Comments
 (0)