**Source code:** :source:`Lib/json/__init__.py`
+.. testsetup:: *
+
+ import json
+ from json import AttrDict
+
--------------
`JSON (JavaScript Object Notation) <https://json.org>`_, specified by
.. 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.load` and :func:`json.loads`::
+
+ .. doctest::
+
+ >>> 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'])
+
+ 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 a dictionary method, then a dictionary
+ lookup finds the key and an attribute lookup finds the method:
+
+ .. doctest::
+
+ >>> 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
----------------------------------------
"""
__version__ = '2.0.9'
__all__ = [
- 'dump', 'dumps', 'load', 'loads',
+ 'dump', 'dumps', 'load', 'loads', 'AttrDict',
'JSONDecoder', 'JSONDecodeError', 'JSONEncoder',
]
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():
+
+ >>> from json import loads, AttrDict
+ >>> json_string = '{"mercury": 88, "venus": 225, "earth": 365, "mars": 687}'
+ >>> orbital_period = 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'])
+
+ 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:
+
+ >>> d = AttrDict(items=50)
+ >>> d['items'] # Lookup the key
+ 50
+ >>> d.items() # Call the method
+ dict_items([('items', 50)])
+
+ """
+ __slots__ = ()
+
+ 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))
--- /dev/null
+from test.test_json import PyTest
+import pickle
+import sys
+import unittest
+
+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(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)
+ with self.assertRaises(AttributeError):
+ d.z
+
+ def test_setattr(self):
+ 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 = 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 = self.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(self.AttrDict(x=1, y=2)),
+ repr(dict(x=1, y=2)))
+
+ def test_overlapping_keys_and_methods(self):
+ d = self.AttrDict(items=50)
+ self.assertEqual(d['items'], 50)
+ self.assertEqual(d.items(), dict(d).items())
+
+ def test_invalid_attribute_names(self):
+ d = self.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):
+ 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
+ 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()))
+
+ # 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()