From a088e3c76cb04b783083aac1f10bd97eb9e9193f Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 19 Aug 2022 22:34:48 -0500 Subject: [PATCH 01/14] Add AttrDict to the JSON module. --- Lib/json/__init__.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index e4c21daaf3e47f..b72b51e2fc2db0 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -357,3 +357,56 @@ def loads(s, *, cls=None, object_hook=None, parse_float=None, if parse_constant is not None: kw['parse_constant'] = parse_constant return cls(**kw).decode(s) + +class AttrDict(dict): + """Dict like object that supports attribute style dotted access. + + This class is intended for use with the *object_hook* in json.loads(): + + >>> json_string = '{"mercury": 88, "venus": 225, "earth": 365, "mars": 687}' + >>> orbital_period = json.loads(json_string, object_hook=AttrDict) + >>> orbital_period['earth'] # Dict style lookup + 365 + >>> orbital_period.earth # Attribute style lookup + 365 + >>> orbital_period.keys() # All dict methods are present + dict_keys(['mercury', 'venus', 'earth', 'mars']) + + For keys that are not valid attribute names, Python syntax only allows + dictionary style access: + + >>> d = AttrDict({'two words': 2}) + >>> d['two words'] # Normal dictionary lookup works + 2 + >>> d.two words # Attribute names cannot contain spaces + ... + SyntaxError: invalid syntax + + If a key has the same name as dictionary method, then a dictionary + lookup finds the key and an attribute lookup finds the method: + + >>> d = AttrDict(items=50) + >>> d['items'] # Lookup the key + 50 + >>> d.items() # Call the method + dict_items([('items', 50)]) + + """ + + def __getattr__(self, attr): + try: + return self[attr] + except KeyError: + raise AttributeError(attr) from None + + def __setattr__(self, attr, value): + self[attr] = value + + def __delattr__(self, attr): + try: + del self[attr] + except KeyError: + raise AttributeError(attr) from None + + def __dir__(self): + return list(self) + dir(type(self)) From 4543ec034ea0a79ea7485419f1a7fbe7bb914274 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 11:28:40 -0500 Subject: [PATCH 02/14] Add tests --- Lib/test/test_attrdict.py | 110 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Lib/test/test_attrdict.py diff --git a/Lib/test/test_attrdict.py b/Lib/test/test_attrdict.py new file mode 100644 index 00000000000000..04940b1d3e28fb --- /dev/null +++ b/Lib/test/test_attrdict.py @@ -0,0 +1,110 @@ +import unittest +import json +from json import AttrDict + +kepler_dict = { + "orbital_period": { + "Mercury": 88, + "Venus": 225, + "Earth": 365, + "Mars": 687, + "Jupiter": 4331, + "Saturn": 10_756, + "Uranus": 30_687, + "Neptune": 60_190 + }, + "dist_from_sun": { + "Mercury": 58, + "Venus": 108, + "Earth": 150, + "Mars": 228, + "Jupiter": 778, + "Saturn": 1_400, + "Uranus": 2_900, + "Neptune": 4_500 + } +} + +class TestAttrDict(unittest.TestCase): + + def test_dict_subclass(self): + self.assertTrue(issubclass(AttrDict, dict)) + + def test_getattr(self): + d = AttrDict(x=1, y=2) + self.assertEqual(d.x, 1) + with self.assertRaises(AttributeError): + d.z + + def test_setattr(self): + d = AttrDict(x=1, y=2) + d.x = 3 + d.z = 5 + self.assertEqual(d, dict(x=3, y=2, z=5)) + + def test_delattr(self): + d = AttrDict(x=1, y=2) + del d.x + self.assertEqual(d, dict(y=2)) + with self.assertRaises(AttributeError): + del d.z + + def test_dir(self): + d = AttrDict(x=1, y=2) + self.assertTrue(set(dir(d)), set(dir(dict)).union({'x', 'y'})) + + def test_repr(self): + # This repr is doesn't round-trip. It matches a regular dict. + # That seems to be the norm for AttrDict recipes being used + # in the wild. Also it supports the design concept that an + # AttrDict is just like a regular dict but has optional + # attribute style lookup. + self.assertEqual(repr(AttrDict(x=1, y=2)), + repr(dict(x=1, y=2))) + + def test_overlapping_keys_and_methods(self): + d = AttrDict(items=50) + self.assertEqual(d['items'], 50) + self.assertEqual(d.items(), dict(d).items()) + + def test_invalid_attribute_names(self): + d = AttrDict({ + 'control': 'normal case', + 'class': 'keyword', + 'two words': 'contains space', + 'hypen-ate': 'contains a hyphen' + }) + self.assertEqual(d.control, dict(d)['control']) + self.assertEqual(d['class'], dict(d)['class']) + self.assertEqual(d['two words'], dict(d)['two words']) + self.assertEqual(d['hypen-ate'], dict(d)['hypen-ate']) + + def test_object_hook_use_case(self): + json_string = json.dumps(kepler_dict) + kepler_ad = json.loads(json_string, object_hook=AttrDict) + + self.assertEqual(kepler_ad, kepler_dict) # Match regular dict + self.assertIsInstance(kepler_ad, AttrDict) # Verify conversion + self.assertIsInstance(kepler_ad.orbital_period, AttrDict) # Nested + + # Exercise dotted lookups + self.assertEqual(kepler_ad.orbital_period, kepler_dict['orbital_period']) + self.assertEqual(kepler_ad.orbital_period.Earth, + kepler_dict['orbital_period']['Earth']) + self.assertEqual(kepler_ad['orbital_period'].Earth, + kepler_dict['orbital_period']['Earth']) + + # Dict style error handling and Attribute style error handling + with self.assertRaises(KeyError): + kepler_ad.orbital_period['Pluto'] + with self.assertRaises(AttributeError): + kepler_ad.orbital_period.Pluto + + # Order preservation + self.assertEqual(list(kepler_ad.items()), list(kepler_dict.items())) + self.assertEqual(list(kepler_ad.orbital_period.items()), + list(kepler_dict['orbital_period'].items())) + + +if __name__ == "__main__": + unittest.main() From b56231b5a82808f629d19cb20ebc12f05a0bce03 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 11:55:56 -0500 Subject: [PATCH 03/14] Add docs --- Doc/library/json.rst | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index f65be85d31bf19..c3ac3f6a00128f 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -532,6 +532,44 @@ Exceptions .. versionadded:: 3.5 +.. class:: AttrDict(**kwargs) + AttrDict(mapping, **kwargs) + AttrDict(iterable, **kwargs) + + Subclass of :class:`dict` object that also supports attribute style dotted access. + + This class is intended for use with the :attr:`object_hook` in :func:`json.loads`:: + + >>> json_string = '{"mercury": 88, "venus": 225, "earth": 365, "mars": 687}' + >>> orbital_period = json.loads(json_string, object_hook=AttrDict) + >>> orbital_period['earth'] # Dict style lookup + 365 + >>> orbital_period.earth # Attribute style lookup + 365 + >>> orbital_period.keys() # All dict methods are present + dict_keys(['mercury', 'venus', 'earth', 'mars']) + + For keys that are not valid attribute names, Python syntax only allows + dictionary style access:: + + >>> d = AttrDict({'two words': 2}) + >>> d['two words'] # Normal dictionary lookup works + 2 + >>> d.two words # Attribute names cannot contain spaces + ... + SyntaxError: invalid syntax + + If a key has the same name as dictionary method, then a dictionary + lookup finds the key and an attribute lookup finds the method:: + + >>> d = AttrDict(items=50) + >>> d['items'] # Lookup the key + 50 + >>> d.items() # Call the method + dict_items([('items', 50)]) + + .. versionadded:: 3.12 + Standard Compliance and Interoperability ---------------------------------------- From 185e8e3d4b2443bb216e9328d704676c8b0afbfe Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 12:48:10 -0500 Subject: [PATCH 04/14] Lowercase names look nicer --- Lib/test/test_attrdict.py | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_attrdict.py b/Lib/test/test_attrdict.py index 04940b1d3e28fb..fc8a4207b73c71 100644 --- a/Lib/test/test_attrdict.py +++ b/Lib/test/test_attrdict.py @@ -4,24 +4,24 @@ kepler_dict = { "orbital_period": { - "Mercury": 88, - "Venus": 225, - "Earth": 365, - "Mars": 687, - "Jupiter": 4331, - "Saturn": 10_756, - "Uranus": 30_687, - "Neptune": 60_190 + "mercury": 88, + "venus": 225, + "earth": 365, + "mars": 687, + "jupiter": 4331, + "saturn": 10_756, + "uranus": 30_687, + "neptune": 60_190 }, "dist_from_sun": { - "Mercury": 58, - "Venus": 108, - "Earth": 150, - "Mars": 228, - "Jupiter": 778, - "Saturn": 1_400, - "Uranus": 2_900, - "Neptune": 4_500 + "mercury": 58, + "venus": 108, + "earth": 150, + "mars": 228, + "jupiter": 778, + "saturn": 1_400, + "uranus": 2_900, + "neptune": 4_500 } } @@ -89,14 +89,14 @@ def test_object_hook_use_case(self): # Exercise dotted lookups self.assertEqual(kepler_ad.orbital_period, kepler_dict['orbital_period']) - self.assertEqual(kepler_ad.orbital_period.Earth, - kepler_dict['orbital_period']['Earth']) - self.assertEqual(kepler_ad['orbital_period'].Earth, - kepler_dict['orbital_period']['Earth']) + self.assertEqual(kepler_ad.orbital_period.earth, + kepler_dict['orbital_period']['earth']) + self.assertEqual(kepler_ad['orbital_period'].earth, + kepler_dict['orbital_period']['earth']) # Dict style error handling and Attribute style error handling with self.assertRaises(KeyError): - kepler_ad.orbital_period['Pluto'] + kepler_ad.orbital_period['pluto'] with self.assertRaises(AttributeError): kepler_ad.orbital_period.Pluto From fcdb764bd9dc1925d6b06b4dda40a30b4d6fc091 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 12:56:36 -0500 Subject: [PATCH 05/14] Add blurb --- .../next/Library/2022-08-20-12-56-15.gh-issue-96145.8ah3pE.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-08-20-12-56-15.gh-issue-96145.8ah3pE.rst diff --git a/Misc/NEWS.d/next/Library/2022-08-20-12-56-15.gh-issue-96145.8ah3pE.rst b/Misc/NEWS.d/next/Library/2022-08-20-12-56-15.gh-issue-96145.8ah3pE.rst new file mode 100644 index 00000000000000..540ec8b71ebf90 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-08-20-12-56-15.gh-issue-96145.8ah3pE.rst @@ -0,0 +1 @@ +Add AttrDict to JSON module for use with object_hook. From 0220c0f10067fea4e62b8d80ee4102c213c352c7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 13:34:30 -0500 Subject: [PATCH 06/14] Add doctest --- Doc/library/json.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index c3ac3f6a00128f..46ca2b12f35823 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -9,6 +9,10 @@ **Source code:** :source:`Lib/json/__init__.py` +.. testsetup:: * + + import json + -------------- `JSON (JavaScript Object Notation) `_, specified by @@ -538,7 +542,10 @@ Exceptions Subclass of :class:`dict` object that also supports attribute style dotted access. - This class is intended for use with the :attr:`object_hook` in :func:`json.loads`:: + This class is intended for use with the :attr:`object_hook` in + :func:`json.loads`: + + .. doctest:: >>> json_string = '{"mercury": 88, "venus": 225, "earth": 365, "mars": 687}' >>> orbital_period = json.loads(json_string, object_hook=AttrDict) @@ -560,7 +567,9 @@ Exceptions SyntaxError: invalid syntax If a key has the same name as dictionary method, then a dictionary - lookup finds the key and an attribute lookup finds the method:: + lookup finds the key and an attribute lookup finds the method: + + .. doctest:: >>> d = AttrDict(items=50) >>> d['items'] # Lookup the key From ae4399a2c542ee003b84c7415acc40947a0703cd Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 16:05:20 -0500 Subject: [PATCH 07/14] Doctest needs an import --- Doc/library/json.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index 46ca2b12f35823..4d57f125d400de 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -12,6 +12,7 @@ .. testsetup:: * import json + from json import AttrDict -------------- From 148e110d2c94dfe39edf81d07d26ba922112b1eb Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 16:14:25 -0500 Subject: [PATCH 08/14] Move tests to the test_json directory --- Lib/test/{ => test_json}/test_attrdict.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Lib/test/{ => test_json}/test_attrdict.py (100%) diff --git a/Lib/test/test_attrdict.py b/Lib/test/test_json/test_attrdict.py similarity index 100% rename from Lib/test/test_attrdict.py rename to Lib/test/test_json/test_attrdict.py From dd8e67cb4c5aae2560450f83190d8f3ed241a3c8 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 16:32:56 -0500 Subject: [PATCH 09/14] Tighten word working in the section for valid attribute names. --- Doc/library/json.rst | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index 4d57f125d400de..d509b11ffdd547 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -544,7 +544,7 @@ Exceptions Subclass of :class:`dict` object that also supports attribute style dotted access. This class is intended for use with the :attr:`object_hook` in - :func:`json.loads`: + :func:`json.load` and :func:`json.loads`: .. doctest:: @@ -557,15 +557,10 @@ Exceptions >>> orbital_period.keys() # All dict methods are present dict_keys(['mercury', 'venus', 'earth', 'mars']) - For keys that are not valid attribute names, Python syntax only allows - dictionary style access:: - - >>> d = AttrDict({'two words': 2}) - >>> d['two words'] # Normal dictionary lookup works - 2 - >>> d.two words # Attribute names cannot contain spaces - ... - SyntaxError: invalid syntax + Attribute style access only works for keys that are valid attribute + names. In contrast, dictionary style access works for all keys. For + example, ``d.two words`` contains a space and is not syntactically + valid Python, so ``d["two words"]`` should be used instead. If a key has the same name as dictionary method, then a dictionary lookup finds the key and an attribute lookup finds the method: From 410d7066c60eb1abe5a7be6f7f915b194dd6bd03 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 16:58:41 -0500 Subject: [PATCH 10/14] Adjust to test requirements in test/json --- Doc/library/json.rst | 6 +++--- Lib/json/__init__.py | 16 ++++++--------- Lib/test/test_json/__init__.py | 1 + Lib/test/test_json/test_attrdict.py | 30 ++++++++++++++--------------- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index d509b11ffdd547..de1dcf2174de93 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -11,8 +11,8 @@ .. testsetup:: * - import json - from json import AttrDict + import json + from json import AttrDict -------------- @@ -544,7 +544,7 @@ Exceptions Subclass of :class:`dict` object that also supports attribute style dotted access. This class is intended for use with the :attr:`object_hook` in - :func:`json.load` and :func:`json.loads`: + :func:`json.load` and :func:`json.loads`:: .. doctest:: diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index b72b51e2fc2db0..217ee43620e20c 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -363,8 +363,9 @@ class AttrDict(dict): This class is intended for use with the *object_hook* in json.loads(): + >>> from json import loads, AttrDict >>> json_string = '{"mercury": 88, "venus": 225, "earth": 365, "mars": 687}' - >>> orbital_period = json.loads(json_string, object_hook=AttrDict) + >>> orbital_period = loads(json_string, object_hook=AttrDict) >>> orbital_period['earth'] # Dict style lookup 365 >>> orbital_period.earth # Attribute style lookup @@ -372,15 +373,10 @@ class AttrDict(dict): >>> orbital_period.keys() # All dict methods are present dict_keys(['mercury', 'venus', 'earth', 'mars']) - For keys that are not valid attribute names, Python syntax only allows - dictionary style access: - - >>> d = AttrDict({'two words': 2}) - >>> d['two words'] # Normal dictionary lookup works - 2 - >>> d.two words # Attribute names cannot contain spaces - ... - SyntaxError: invalid syntax + Attribute style access only works for keys that are valid attribute names. + In contrast, dictionary style access works for all keys. + For example, ``d.two words`` contains a space and is not syntactically + valid Python, so ``d["two words"]`` should be used instead. If a key has the same name as dictionary method, then a dictionary lookup finds the key and an attribute lookup finds the method: diff --git a/Lib/test/test_json/__init__.py b/Lib/test/test_json/__init__.py index 74b64ed86a3183..37b2e0d5e26d16 100644 --- a/Lib/test/test_json/__init__.py +++ b/Lib/test/test_json/__init__.py @@ -18,6 +18,7 @@ class PyTest(unittest.TestCase): json = pyjson loads = staticmethod(pyjson.loads) dumps = staticmethod(pyjson.dumps) + AttrDict = pyjson.AttrDict JSONDecodeError = staticmethod(pyjson.JSONDecodeError) @unittest.skipUnless(cjson, 'requires _json') diff --git a/Lib/test/test_json/test_attrdict.py b/Lib/test/test_json/test_attrdict.py index fc8a4207b73c71..ac5f81d2e6bee0 100644 --- a/Lib/test/test_json/test_attrdict.py +++ b/Lib/test/test_json/test_attrdict.py @@ -1,6 +1,5 @@ import unittest -import json -from json import AttrDict +from test.test_json import PyTest kepler_dict = { "orbital_period": { @@ -11,7 +10,7 @@ "jupiter": 4331, "saturn": 10_756, "uranus": 30_687, - "neptune": 60_190 + "neptune": 60_190, }, "dist_from_sun": { "mercury": 58, @@ -21,36 +20,36 @@ "jupiter": 778, "saturn": 1_400, "uranus": 2_900, - "neptune": 4_500 + "neptune": 4_500, } } -class TestAttrDict(unittest.TestCase): +class TestAttrDict(PyTest): def test_dict_subclass(self): - self.assertTrue(issubclass(AttrDict, dict)) + self.assertTrue(issubclass(self.AttrDict, dict)) def test_getattr(self): - d = AttrDict(x=1, y=2) + d = self.AttrDict(x=1, y=2) self.assertEqual(d.x, 1) with self.assertRaises(AttributeError): d.z def test_setattr(self): - d = AttrDict(x=1, y=2) + d = self.AttrDict(x=1, y=2) d.x = 3 d.z = 5 self.assertEqual(d, dict(x=3, y=2, z=5)) def test_delattr(self): - d = AttrDict(x=1, y=2) + d = self.AttrDict(x=1, y=2) del d.x self.assertEqual(d, dict(y=2)) with self.assertRaises(AttributeError): del d.z def test_dir(self): - d = AttrDict(x=1, y=2) + d = self.AttrDict(x=1, y=2) self.assertTrue(set(dir(d)), set(dir(dict)).union({'x', 'y'})) def test_repr(self): @@ -59,16 +58,16 @@ def test_repr(self): # in the wild. Also it supports the design concept that an # AttrDict is just like a regular dict but has optional # attribute style lookup. - self.assertEqual(repr(AttrDict(x=1, y=2)), + self.assertEqual(repr(self.AttrDict(x=1, y=2)), repr(dict(x=1, y=2))) def test_overlapping_keys_and_methods(self): - d = AttrDict(items=50) + d = self.AttrDict(items=50) self.assertEqual(d['items'], 50) self.assertEqual(d.items(), dict(d).items()) def test_invalid_attribute_names(self): - d = AttrDict({ + d = self.AttrDict({ 'control': 'normal case', 'class': 'keyword', 'two words': 'contains space', @@ -80,8 +79,9 @@ def test_invalid_attribute_names(self): self.assertEqual(d['hypen-ate'], dict(d)['hypen-ate']) def test_object_hook_use_case(self): - json_string = json.dumps(kepler_dict) - kepler_ad = json.loads(json_string, object_hook=AttrDict) + AttrDict = self.AttrDict + json_string = self.dumps(kepler_dict) + kepler_ad = self.loads(json_string, object_hook=AttrDict) self.assertEqual(kepler_ad, kepler_dict) # Match regular dict self.assertIsInstance(kepler_ad, AttrDict) # Verify conversion From 2cb96428bc6fa9b21be925d14c4166467351e0b5 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 19:15:14 -0500 Subject: [PATCH 11/14] Add constructor tests and slots --- Lib/json/__init__.py | 1 + Lib/test/test_json/test_attrdict.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 217ee43620e20c..25304cde345d2d 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -388,6 +388,7 @@ class AttrDict(dict): dict_items([('items', 50)]) """ + __slots__ = () def __getattr__(self, attr): try: diff --git a/Lib/test/test_json/test_attrdict.py b/Lib/test/test_json/test_attrdict.py index ac5f81d2e6bee0..647cd60edf2db5 100644 --- a/Lib/test/test_json/test_attrdict.py +++ b/Lib/test/test_json/test_attrdict.py @@ -29,6 +29,20 @@ class TestAttrDict(PyTest): def test_dict_subclass(self): self.assertTrue(issubclass(self.AttrDict, dict)) + def test_slots(self): + d = self.AttrDict(x=1, y=2) + with self.assertRaises(TypeError): + vars(d) + + def test_constructor_signatures(self): + AttrDict = self.AttrDict + target = dict(x=1, y=2) + self.assertEqual(AttrDict(x=1, y=2), target) # kwargs + self.assertEqual(AttrDict(dict(x=1, y=2)), target) # mapping + self.assertEqual(AttrDict(dict(x=1, y=0), y=2), target) # mapping, kwargs + self.assertEqual(AttrDict([('x', 1), ('y', 2)]), target) # iterable + self.assertEqual(AttrDict([('x', 1), ('y', 0)], y=2), target) # iterable, kwargs + def test_getattr(self): d = self.AttrDict(x=1, y=2) self.assertEqual(d.x, 1) From aa2b7a5ac2c3932322e39d31e9cb3f5767c669f8 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Aug 2022 20:40:32 -0500 Subject: [PATCH 12/14] Grammar fix --- Doc/library/json.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/json.rst b/Doc/library/json.rst index de1dcf2174de93..467d5d9e1544d4 100644 --- a/Doc/library/json.rst +++ b/Doc/library/json.rst @@ -562,7 +562,7 @@ Exceptions example, ``d.two words`` contains a space and is not syntactically valid Python, so ``d["two words"]`` should be used instead. - If a key has the same name as dictionary method, then a dictionary + If a key has the same name as a dictionary method, then a dictionary lookup finds the key and an attribute lookup finds the method: .. doctest:: From 2eb6f5ed93edfd98438c70b2a4b9ff42eaec83ec Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sun, 21 Aug 2022 13:31:40 -0500 Subject: [PATCH 13/14] Update __all__. Add a roundtrip test. --- Lib/json/__init__.py | 2 +- Lib/test/test_json/test_attrdict.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 25304cde345d2d..d775fb1c11071d 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -97,7 +97,7 @@ """ __version__ = '2.0.9' __all__ = [ - 'dump', 'dumps', 'load', 'loads', + 'dump', 'dumps', 'load', 'loads', 'AttrDict', 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', ] diff --git a/Lib/test/test_json/test_attrdict.py b/Lib/test/test_json/test_attrdict.py index 647cd60edf2db5..ab40563db5aaf8 100644 --- a/Lib/test/test_json/test_attrdict.py +++ b/Lib/test/test_json/test_attrdict.py @@ -119,6 +119,9 @@ def test_object_hook_use_case(self): self.assertEqual(list(kepler_ad.orbital_period.items()), list(kepler_dict['orbital_period'].items())) + # Round trip + self.assertEqual(self.dumps(kepler_ad), json_string) + if __name__ == "__main__": unittest.main() From ad9e5e90d6a9540609dd5f17e9a7a23c05f5376e Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sun, 21 Aug 2022 14:07:52 -0500 Subject: [PATCH 14/14] Add pickle tests --- Lib/test/test_json/test_attrdict.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_attrdict.py b/Lib/test/test_json/test_attrdict.py index ab40563db5aaf8..48d14f4db93c12 100644 --- a/Lib/test/test_json/test_attrdict.py +++ b/Lib/test/test_json/test_attrdict.py @@ -1,5 +1,7 @@ -import unittest from test.test_json import PyTest +import pickle +import sys +import unittest kepler_dict = { "orbital_period": { @@ -122,6 +124,22 @@ def test_object_hook_use_case(self): # Round trip self.assertEqual(self.dumps(kepler_ad), json_string) + def test_pickle(self): + AttrDict = self.AttrDict + json_string = self.dumps(kepler_dict) + kepler_ad = self.loads(json_string, object_hook=AttrDict) + + # Pickling requires the cached module to be the real module + cached_module = sys.modules.get('json') + sys.modules['json'] = self.json + try: + for protocol in range(6): + kepler_ad2 = pickle.loads(pickle.dumps(kepler_ad, protocol)) + self.assertEqual(kepler_ad2, kepler_ad) + self.assertEqual(type(kepler_ad2), AttrDict) + finally: + sys.modules['json'] = cached_module + if __name__ == "__main__": unittest.main()