Skip to content

Instantly share code, notes, and snippets.

@tanbro
Last active January 17, 2024 14:51
Show Gist options
  • Save tanbro/4712943 to your computer and use it in GitHub Desktop.
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.
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()
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)
@legout
Copy link

legout commented Oct 24, 2013

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment