Last active
January 17, 2024 14:51
-
-
Save tanbro/4712943 to your computer and use it in GitHub Desktop.
A class with both dictionary and attribute style access to it's data member.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
from typing import Any, Dict, Iterable, Mapping, MutableMapping, MutableSequence, Union | |
class AttrDict: | |
""" | |
A class with both dictionary and attribute style access to it's data member. | |
It can be used in `RestrictedPython <http://restrictedpython.readthedocs.io/>`_ | |
eg:: | |
>>> d = AttrDict() | |
>>> d.a = 'this is a' | |
>>> d['a'] | |
'this is a' | |
>>> d['b'] = 'this is b' | |
>>> d.b # noqa | |
'this is b' | |
>>> d('a') | |
'this is a' | |
>>> d('c', 'default value when not exists') | |
'default value when not exists' | |
>>> print(d('c')) | |
None | |
>>> 'b' in d | |
True | |
>>> list(d) | |
['a', 'b] | |
>>> len(d) | |
2 | |
""" | |
def __init__( | |
self, | |
initial_data: Union[Mapping[str, Any], AttrDict, None] = None, | |
ignore_not_allowed_names: bool = False, | |
/, | |
**kwargs, | |
): | |
if isinstance(initial_data, AttrDict): | |
for k in initial_data: | |
if self.__name_allowed__(k, throw=not ignore_not_allowed_names): | |
setattr(self, k, initial_data[k]) | |
elif initial_data is not None: | |
for k, v in initial_data.items(): | |
if self.__name_allowed__(k, throw=not ignore_not_allowed_names): | |
setattr(self, k, v) | |
for k, v in kwargs.items(): | |
if self.__name_allowed__(k, throw=not ignore_not_allowed_names): | |
setattr(self, k, v) | |
def __setattr__(self, name, value): | |
self.__name_allowed__(name, throw=True) | |
super().__setattr__(name, value) | |
def __getitem__(self, name: str) -> Any: | |
self.__name_allowed__(name, throw=True) | |
try: | |
return getattr(self, name) | |
except AttributeError: | |
raise KeyError(name) | |
def __setitem__(self, name: str, value): | |
self.__name_allowed__(name, throw=True) | |
try: | |
setattr(self, name, value) | |
except AttributeError: | |
raise KeyError(name) | |
def __delitem__(self, name: str): | |
self.__name_allowed__(name, throw=True) | |
try: | |
delattr(self, name) | |
except AttributeError: | |
raise KeyError(name) | |
def __call__(self, name: str, default=None): | |
self.__name_allowed__(name, throw=True) | |
return getattr(self, name, default) | |
def __contains__(self, name: str): | |
self.__name_allowed__(name, throw=True) | |
return hasattr(self, name) | |
def __iter__(self): | |
return (k for k in dir(self) if self.__name_allowed__(k, throw=False)) | |
def __len__(self): | |
return sum(1 for _ in self) | |
def __str__(self): | |
return "<{} object at 0x{:x}>".format(self.__class__.__name__, id(self)) | |
def __repr__(self): | |
s = " ".join("{}={!r}".format(k, getattr(self, k)) for k in self.__iter__()) | |
return "<{} object at 0x{:x} {}>".format(self.__class__.__name__, id(self), s) | |
def __or__(self, other: Union[Mapping[str, Any], AttrDict]): | |
res = AttrDict(self) | |
if isinstance(other, AttrDict): | |
for k in other: | |
if self.__name_allowed__(k): | |
setattr(res, k, other[k]) | |
else: | |
for k, v in other.items(): | |
if self.__name_allowed__(k): | |
setattr(res, k, v) | |
return res | |
def __ror__(self, other: Union[Mapping[str, Any], AttrDict]): | |
res = self.__asdict__() | |
if isinstance(other, AttrDict): | |
res.update(other.__asdict__()) | |
else: | |
res.update(other) | |
return res | |
def __asdict__(self): | |
return {k: self[k] for k in self} | |
@staticmethod | |
def __name_allowed__(name, throw=False): | |
if ( | |
name.startswith("__") | |
and name.endswith("__") | |
and any(c != "_" for c in name) | |
): | |
if throw: | |
raise AttributeError(f"attribute {name!r} is not allowed") | |
return False | |
return True | |
def __guarded_setitem__(self, name, value): | |
self.__setitem__(name, value) | |
def __guarded_delitem__(self, name): | |
self.__delitem__(name) | |
def __guarded_setattr__(self, name, value): | |
self.__setattr__(name, value) | |
def __guarded_delattr__(self, name): | |
self.__delattr__(name) | |
def update( | |
obj: AttrDict, other: Union[AttrDict, Mapping[str, Any], None] = None, /, **kwargs | |
) -> AttrDict: | |
if other is not None: | |
obj |= other | |
obj |= kwargs | |
return obj | |
def asdict(obj: AttrDict) -> Dict[str, Any]: | |
return obj.__asdict__() | |
def convert(obj): | |
"""Walks a simple data structure, converting dictionary to AttrDict **recursively**. | |
Supports lists, tuples, and dictionaries. | |
""" | |
if isinstance(obj, Mapping): | |
return AttrDict({k: convert(v) for (k, v) in obj.items()}) | |
elif isinstance(obj, Iterable) and not isinstance( | |
obj, (bytearray, bytes, memoryview, str) | |
): | |
return [convert(i) for i in obj] | |
else: | |
return obj | |
def extract(obj, inplace: bool = False): | |
""" | |
Recursively extract :class:`.AttrDict` object to into :class:`dict`. | |
Args: | |
obj: | |
Single :class:`.AttrDict` object or a Dict / List like object with :class:`.AttrDict` object(s) in it . | |
""" | |
if isinstance(obj, AttrDict): | |
d = asdict(obj) | |
return extract(d, inplace=inplace) | |
elif isinstance(obj, Mapping): | |
if inplace: | |
if not isinstance(obj, MutableMapping): | |
raise ValueError( | |
f"{type(obj).__name__} is not mutable, and can not perform an in-place extraction" | |
) | |
for k, v in obj.items(): | |
obj[k] = extract(v, inplace=True) | |
else: | |
return {k: extract(v, inplace=False) for k, v in obj.items()} | |
elif isinstance(obj, Iterable) and not isinstance( | |
obj, (bytearray, bytes, memoryview, str) | |
): | |
if inplace: | |
if not isinstance(obj, MutableSequence): | |
raise ValueError( | |
f"{type(obj).__name__} is not mutable, and can not perform an in-place extraction" | |
) | |
for i, v in enumerate(obj): | |
obj[i] = extract(v, inplace=True) | |
else: | |
return [extract(m, inplace=False) for m in obj] | |
return obj | |
if __name__ == "__main__": | |
import unittest | |
class TestAttrDict(unittest.TestCase): | |
def test_construct_from_dict(self): | |
obj = AttrDict({"a": "A", "b": "B"}) | |
self.assertEqual(obj.a, "A") # type:ignore | |
self.assertEqual(obj.b, "B") # type:ignore | |
self.assertEqual(obj["a"], "A") | |
self.assertEqual(obj["b"], "B") | |
self.assertEqual(obj("a"), "A") | |
self.assertEqual(obj("b"), "B") | |
self.assertEqual(obj("c"), None) | |
self.assertEqual(obj("c", ""), "") | |
self.assertTrue("a" in obj) | |
self.assertTrue("b" in obj) | |
self.assertFalse("c" in obj) | |
def test_construct_from_nested_dict(self): | |
obj = AttrDict({"a": "A", "b": "B", "c": {"c1": "C1", "c2": "C2"}}) | |
self.assertEqual(obj.a, "A") # type:ignore | |
self.assertEqual(obj.b, "B") # type:ignore | |
self.assertEqual(obj["a"], "A") | |
self.assertEqual(obj["b"], "B") | |
self.assertTrue("a" in obj) | |
self.assertTrue("b" in obj) | |
self.assertTrue("c" in obj) | |
self.assertDictEqual(obj["c"], {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertDictEqual(obj.c, {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertEqual(obj["c"]["c1"], "C1") # type:ignore | |
self.assertEqual(obj["c"]["c2"], "C2") # type:ignore | |
self.assertEqual(obj.c["c1"], "C1") # type:ignore | |
self.assertEqual(obj.c["c2"], "C2") # type:ignore | |
self.assertFalse("d" in obj) | |
def test_construct_from_other(self): | |
obj = AttrDict( | |
AttrDict({"a": "A", "b": "B", "c": {"c1": "C1", "c2": "C2"}}) | |
) | |
self.assertEqual(obj.a, "A") # type:ignore | |
self.assertEqual(obj.b, "B") # type:ignore | |
self.assertEqual(obj["a"], "A") | |
self.assertEqual(obj["b"], "B") | |
self.assertTrue("a" in obj) | |
self.assertTrue("b" in obj) | |
self.assertTrue("c" in obj) | |
self.assertDictEqual(obj["c"], {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertDictEqual(obj.c, {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertEqual(obj["c"]["c1"], "C1") # type:ignore | |
self.assertEqual(obj["c"]["c2"], "C2") # type:ignore | |
self.assertEqual(obj.c["c1"], "C1") # type:ignore | |
self.assertEqual(obj.c["c2"], "C2") # type:ignore | |
self.assertFalse("d" in obj) | |
def test_construct_kwargs(self): | |
obj = AttrDict(**{"a": "A", "b": "B", "c": {"c1": "C1", "c2": "C2"}}) | |
self.assertEqual(obj.a, "A") # type:ignore | |
self.assertEqual(obj.b, "B") # type:ignore | |
self.assertEqual(obj["a"], "A") | |
self.assertEqual(obj["b"], "B") | |
self.assertTrue("a" in obj) | |
self.assertTrue("b" in obj) | |
self.assertTrue("c" in obj) | |
self.assertDictEqual(obj["c"], {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertDictEqual(obj.c, {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertEqual(obj["c"]["c1"], "C1") # type:ignore | |
self.assertEqual(obj["c"]["c2"], "C2") # type:ignore | |
self.assertEqual(obj.c["c1"], "C1") # type:ignore | |
self.assertEqual(obj.c["c2"], "C2") # type:ignore | |
self.assertFalse("d" in obj) | |
def test_non_ascii_name(self): | |
obj = AttrDict(属性1="this is 属性1!") | |
self.assertIn("属性1", obj) | |
self.assertEqual(obj["属性1"], obj.属性1) # type:ignore | |
def test_non_attr_name(self): | |
obj = AttrDict({"123": "this is 123!"}) | |
self.assertIn("123", obj) | |
self.assertEqual(obj["123"], "this is 123!") | |
def test_convert(self): | |
d = { | |
"a": "A", | |
"b": "B", | |
"c": {"c1": "C1", "c2": "C2"}, | |
"d": [[{"a": 1, "b": 2, "c": 3}]], | |
} | |
obj = convert(d) | |
self.assertEqual(obj.a, "A") # type:ignore | |
self.assertEqual(obj.b, "B") # type:ignore | |
self.assertEqual(obj["a"], "A") # type:ignore | |
self.assertEqual(obj["b"], "B") # type:ignore | |
self.assertTrue("a" in obj) | |
self.assertTrue("b" in obj) | |
self.assertTrue("c" in obj) | |
self.assertDictEqual( | |
asdict(obj["c"]), {"c1": "C1", "c2": "C2"} # type:ignore | |
) | |
self.assertDictEqual(asdict(obj.c), {"c1": "C1", "c2": "C2"}) # type:ignore | |
self.assertEqual(obj["c"]["c1"], "C1") # type:ignore | |
self.assertEqual(obj["c"]["c2"], "C2") # type:ignore | |
self.assertEqual(obj.c["c1"], "C1") # type:ignore | |
self.assertEqual(obj.c["c2"], "C2") # type:ignore | |
self.assertEqual(obj.c.c1, "C1") # type:ignore | |
self.assertEqual(obj.c.c2, "C2") # type:ignore | |
self.assertDictEqual( | |
asdict(obj.d[0][0]), {"a": 1, "b": 2, "c": 3} # type:ignore | |
) | |
def test_extract(self): | |
d0 = { | |
"a": "A", | |
"b": "B", | |
"c": {"c1": "C1", "c2": "C2"}, | |
"d": [[{"a": 1, "b": 2, "c": 3}]], | |
} | |
obj = convert(d0) | |
d1 = extract(obj) | |
self.assertEqual(d0["a"], d1["a"]) # type:ignore | |
self.assertEqual(d0["b"], d1["b"]) # type:ignore | |
self.assertDictEqual(d0["c"], d1["c"]) # type:ignore | |
self.assertDictEqual(d0["d"][0][0], d1["d"][0][0]) # type:ignore | |
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import attrdict | |
obj = { | |
'A':{ | |
'A1':{ | |
'A11' : [{'a':'this is a'}, {'b':'this is b'}], | |
'A12' : [{'a2':'this is a2'}, {'b2':'this is b2'}] | |
}, | |
'A2':[12, 324, 34.23, None] | |
} | |
} | |
adobj = attrdict.convert(obj) | |
print(adobj) | |
print(adobj.A.A1.A11[1].b) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi,
very nice done. I was thinking about doing this my own, but this works like charme.
However, ipythons autocompletion won´t find the attributes of an Attrdict object. But that was initially my main idea why accessing dictionary keys as attributes would be nice :).
Regards
Legout