Skip to content
Closed
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
12 changes: 6 additions & 6 deletions Lib/json/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,9 @@ def _iterencode_list(lst, _current_indent_level):
yield buf + _floatstr(value)
else:
yield buf
if isinstance(value, (list, tuple)):
if issubclass(type(value), (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, (dict, frozendict)):
elif issubclass(type(value), (dict, frozendict)):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
Expand Down Expand Up @@ -393,9 +393,9 @@ def _iterencode_dict(dct, _current_indent_level):
# see comment for int/float in _make_iterencode
yield _floatstr(value)
else:
if isinstance(value, (list, tuple)):
if issubclass(type(value), (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, (dict, frozendict)):
elif issubclass(type(value), (dict, frozendict)):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
Expand Down Expand Up @@ -427,9 +427,9 @@ def _iterencode(o, _current_indent_level):
elif isinstance(o, float):
# see comment for int/float in _make_iterencode
yield _floatstr(o)
elif isinstance(o, (list, tuple)):
elif issubclass(type(o), (list, tuple)):
yield from _iterencode_list(o, _current_indent_level)
elif isinstance(o, (dict, frozendict)):
elif issubclass(type(o), (dict, frozendict)):
yield from _iterencode_dict(o, _current_indent_level)
else:
if markers is not None:
Expand Down
83 changes: 83 additions & 0 deletions Lib/test/test_json/test_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
from test.test_json import PyTest, CTest


class ClassSpoof:
def __init__(self, spoofed_class):
self.spoofed_class = spoofed_class

@property
def __class__(self):
return self.spoofed_class

def __iter__(self):
return iter(("spoofed",))

def items(self):
return (("spoofed", True),)


class TestDefault:
def test_default(self):
self.assertEqual(
Expand Down Expand Up @@ -36,6 +51,74 @@ def test_ordereddict(self):
self.dumps(od, sort_keys=True),
'{"a": 1, "b": 2, "c": 3, "d": 4}')

def check_spoof_uses_default(
self, value, fallback, expected, spoofed_obj
):
calls = []

def default(candidate):
calls.append(candidate)
return fallback

encoded = self.dumps(value, default=default)
self.assertEqual(encoded, self.dumps(expected))
self.assertEqual(len(calls), 1)
self.assertIs(calls[0], spoofed_obj)

def test_spoofed_containers_use_default_at_top_level(self):
for spoofed_class in (list, tuple, dict, frozendict):
with self.subTest(spoofed_class=spoofed_class):
obj = ClassSpoof(spoofed_class)
fallback = {"defaulted": spoofed_class.__name__}
self.check_spoof_uses_default(
obj, fallback, fallback, obj)

def test_spoofed_containers_use_default_as_list_item(self):
for spoofed_class in (list, tuple, dict, frozendict):
with self.subTest(spoofed_class=spoofed_class):
obj = ClassSpoof(spoofed_class)
fallback = {"defaulted": spoofed_class.__name__}
self.check_spoof_uses_default(
[obj], fallback, [fallback], obj)

def test_spoofed_containers_use_default_as_dict_value(self):
for spoofed_class in (list, tuple, dict, frozendict):
with self.subTest(spoofed_class=spoofed_class):
obj = ClassSpoof(spoofed_class)
fallback = {"defaulted": spoofed_class.__name__}
self.check_spoof_uses_default(
{"value": obj},
fallback, {"value": fallback}, obj)

def test_spoofed_containers_without_default_raise_type_error(self):
for spoofed_class in (list, tuple, dict, frozendict):
with self.subTest(spoofed_class=spoofed_class):
with self.assertRaises(TypeError):
self.dumps(ClassSpoof(spoofed_class))

def test_genuine_container_subclasses_are_encoded(self):
class ListSubclass(list):
pass

class TupleSubclass(tuple):
pass

class DictSubclass(dict):
pass

class FrozenDictSubclass(frozendict):
pass

values = (
(ListSubclass((1, 2)), [1, 2]),
(TupleSubclass((1, 2)), [1, 2]),
(DictSubclass(a=1), {"a": 1}),
(FrozenDictSubclass(a=1), {"a": 1}),
)
for value, expected in values:
with self.subTest(value_type=type(value)):
self.assertEqual(self.dumps(value), self.dumps(expected))


class TestPyDefault(TestDefault, PyTest): pass
class TestCDefault(TestDefault, CTest): pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Make the pure-Python :mod:`json` encoder use an object's actual type hierarchy
rather than a spoofed ``__class__`` when recognizing containers, matching the C
accelerator.
Loading