Skip to content

Commit 792348b

Browse files
authored
Add keyattr_dynamic property (False by default). #261 (#266)
* Add `AttributeError` message. * Add `keyattr_dynamic` property (default `False`). * Update README.md
1 parent 4972b1c commit 792348b

File tree

4 files changed

+151
-28
lines changed

4 files changed

+151
-28
lines changed

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,25 +90,40 @@ page = params.get_int("page", 1)
9090
It is possible to get/set items using **keys as attributes** (dotted notation).
9191

9292
```python
93-
d = benedict()
93+
d = benedict(keyattr_dynamic=True) # default False
9494
d.profile.firstname = "Fabio"
9595
d.profile.lastname = "Caccamo"
9696
print(d) # -> { "profile":{ "firstname":"Fabio", "lastname":"Caccamo" } }
9797
```
9898

99+
By default, if the `keyattr_dynamic` is not explicitly set to `True`, this functionality works for get/set only already existing items.
100+
99101
#### Disable keyattr functionality
100-
You can disable the keyattr functionality passing `keyattr_enabled=False` in the constructor.
102+
You can disable the keyattr functionality passing `keyattr_enabled=False` option in the constructor.
101103

102104
```python
103-
d = benedict(existing_dict, keyattr_enabled=False)
105+
d = benedict(existing_dict, keyattr_enabled=False) # default True
104106
```
105107

106-
You can disable the keyattr functionality using the `getter/setter` property.
108+
or using the `getter/setter` property.
107109

108110
```python
109111
d.keyattr_enabled = False
110112
```
111113

114+
#### Dynamic keyattr functionality
115+
You can enable the dynamic attributes access functionality passing `keyattr_dynamic=True` in the constructor.
116+
117+
```python
118+
d = benedict(existing_dict, keyattr_dynamic=True) # default False
119+
```
120+
121+
or using the `getter/setter` property.
122+
123+
```python
124+
d.keyattr_dynamic = True
125+
```
126+
112127
> **Warning** - even if this feature is very useful, it has some obvious limitations: it works only for string keys that are *unprotected* (not starting with an `_`) and that don't clash with the currently supported methods names.
113128
114129
### Keylist
@@ -173,13 +188,13 @@ d.keypath_separator = "/"
173188
```
174189

175190
#### Disable keypath functionality
176-
You can disable the keypath functionality passing `keypath_separator=None` in the constructor.
191+
You can disable the keypath functionality passing `keypath_separator=None` option in the constructor.
177192

178193
```python
179194
d = benedict(existing_dict, keypath_separator=None)
180195
```
181196

182-
You can disable the keypath functionality using the `getter/setter` property.
197+
or using the `getter/setter` property.
183198

184199
```python
185200
d.keypath_separator = None

benedict/dicts/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(self, *args, **kwargs):
4747
if len(args) == 1 and isinstance(args[0], benedict):
4848
obj = args[0]
4949
kwargs.setdefault("keyattr_enabled", obj.keyattr_enabled)
50+
kwargs.setdefault("keyattr_dynamic", obj.keyattr_dynamic)
5051
kwargs.setdefault("keypath_separator", obj.keypath_separator)
5152
super().__init__(obj.dict(), **kwargs)
5253
return
@@ -56,6 +57,7 @@ def __deepcopy__(self, memo):
5657
obj_type = type(self)
5758
obj = obj_type(
5859
keyattr_enabled=self._keyattr_enabled,
60+
keyattr_dynamic=self._keyattr_dynamic,
5961
keypath_separator=self._keypath_separator,
6062
)
6163
for key, value in self.items():
@@ -78,6 +80,7 @@ def _cast(self, value):
7880
return obj_type(
7981
value,
8082
keyattr_enabled=self._keyattr_enabled,
83+
keyattr_dynamic=self._keyattr_dynamic,
8184
keypath_separator=self._keypath_separator,
8285
check_keys=False,
8386
)

benedict/dicts/keyattr/keyattr_dict.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
class KeyattrDict(BaseDict):
55
_keyattr_enabled = None
6+
_keyattr_dynamic = None
67

78
def __init__(self, *args, **kwargs):
89
self._keyattr_enabled = kwargs.pop("keyattr_enabled", True)
10+
self._keyattr_dynamic = kwargs.pop("keyattr_dynamic", False)
911
super().__init__(*args, **kwargs)
1012

1113
@property
@@ -16,35 +18,45 @@ def keyattr_enabled(self):
1618
def keyattr_enabled(self, value):
1719
self._keyattr_enabled = value
1820

21+
@property
22+
def keyattr_dynamic(self):
23+
return self._keyattr_dynamic
24+
25+
@keyattr_dynamic.setter
26+
def keyattr_dynamic(self, value):
27+
self._keyattr_dynamic = value
28+
1929
def __getattr__(self, attr):
30+
attr_message = f"{self.__class__.__name__!r} object has no attribute {attr!r}"
2031
if not self._keyattr_enabled:
21-
raise AttributeError
22-
# return super().__getattr__(attr)
32+
raise AttributeError(attr_message)
2333
try:
2434
return self.__getitem__(attr)
2535
except KeyError:
2636
if attr.startswith("_"):
27-
raise AttributeError(
28-
f"{self.__class__.__name__!r} object has no attribute {attr!r}"
29-
) from None
37+
raise AttributeError(attr_message) from None
38+
if not self._keyattr_dynamic:
39+
raise AttributeError(attr_message) from None
3040
self.__setitem__(attr, {})
3141
return self.__getitem__(attr)
3242

3343
def __setattr__(self, attr, value):
44+
attr_message = f"{self.__class__.__name__!r} object has no attribute {attr!r}"
3445
if attr in self:
3546
# set existing key
3647
if not self._keyattr_enabled:
37-
raise AttributeError
48+
raise AttributeError(attr_message)
3849
self.__setitem__(attr, value)
3950
elif hasattr(self.__class__, attr):
4051
# set existing attr
4152
super().__setattr__(attr, value)
4253
else:
4354
# set new key
4455
if not self._keyattr_enabled:
45-
raise AttributeError
56+
raise AttributeError(attr_message)
4657
self.__setitem__(attr, value)
4758

4859
def __setstate__(self, state):
4960
super().__setstate__(state)
5061
self._keyattr_enabled = state["_keyattr_enabled"]
62+
self._keyattr_dynamic = state["_keyattr_dynamic"]

tests/dicts/test_benedict_keyattr.py

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,23 @@ def test_getitem(self):
1414
},
1515
},
1616
}
17-
b = benedict(d, keyattr_enabled=True)
17+
b = benedict(d, keyattr_enabled=True, keyattr_dynamic=True)
18+
self.assertEqual(b.a.b.c, "ok")
19+
self.assertEqual(b.a.b.d[-1], 3)
20+
self.assertEqual(b.a.b.e[0].f, 4)
21+
self.assertEqual(b.a.b.e[-1].g[0], 5)
22+
23+
def test_getitem_not_dynamic(self):
24+
d = {
25+
"a": {
26+
"b": {
27+
"c": "ok",
28+
"d": [0, 1, 2, 3],
29+
"e": [{"f": 4}, {"g": [5]}],
30+
},
31+
},
32+
}
33+
b = benedict(d, keyattr_enabled=True, keyattr_dynamic=True)
1834
self.assertEqual(b.a.b.c, "ok")
1935
self.assertEqual(b.a.b.d[-1], 3)
2036
self.assertEqual(b.a.b.e[0].f, 4)
@@ -24,9 +40,17 @@ def test_getitem_with_non_existing_key(self):
2440
d = {
2541
"a": "ok",
2642
}
27-
b = benedict(d, keyattr_enabled=True)
43+
b = benedict(d, keyattr_enabled=True, keyattr_dynamic=True)
2844
self.assertEqual(b.b, {})
2945

46+
def test_getitem_with_non_existing_key_not_dynamic(self):
47+
d = {
48+
"a": "ok",
49+
}
50+
b = benedict(d, keyattr_enabled=True, keyattr_dynamic=False)
51+
with self.assertRaises(AttributeError):
52+
_ = b.b
53+
3054
def test_getitem_with_non_existing_protected_item(self):
3155
d = {
3256
"a": {
@@ -42,6 +66,21 @@ def test_getitem_with_non_existing_protected_item(self):
4266
with self.assertRaises(AttributeError):
4367
b.a.b.__test__()
4468

69+
def test_getitem_with_non_existing_protected_item_not_dynamic(self):
70+
d = {
71+
"a": {
72+
"b": {
73+
"c": "ok",
74+
},
75+
},
76+
}
77+
b = benedict(d, keyattr_enabled=True, keyattr_dynamic=False)
78+
self.assertEqual(b.a.b.c, "ok")
79+
with self.assertRaises(AttributeError):
80+
b.__test__()
81+
with self.assertRaises(AttributeError):
82+
b.a.b.__test__()
83+
4584
def test_getitem_with_keyattr_disabled(self):
4685
d = {
4786
"a": "ok",
@@ -50,6 +89,14 @@ def test_getitem_with_keyattr_disabled(self):
5089
with self.assertRaises(AttributeError):
5190
_ = b.a
5291

92+
def test_getitem_with_keyattr_disabled_not_dynamic(self):
93+
d = {
94+
"a": "ok",
95+
}
96+
b = benedict(d, keyattr_enabled=False)
97+
with self.assertRaises(AttributeError):
98+
_ = b.a
99+
53100
def test_setitem(self):
54101
d = {
55102
"a": {
@@ -60,7 +107,7 @@ def test_setitem(self):
60107
},
61108
},
62109
}
63-
b = benedict(d, keyattr_enabled=True)
110+
b = benedict(d, keyattr_enabled=True, keyattr_dynamic=True)
64111
b.a.b.c = "ok"
65112
self.assertEqual(b.a.b.c, "ok")
66113
b.a.b.d[-1] = 4
@@ -79,25 +126,38 @@ def test_setitem_with_keyattr_disabled(self):
79126
b.a = 2
80127
with self.assertRaises(AttributeError):
81128
b.b = 3
129+
with self.assertRaises(AttributeError):
130+
b.c.d = 3
131+
132+
def test_setitem_with_keyattr_not_dynamic(self):
133+
d = {
134+
"a": 1,
135+
}
136+
b = benedict(d, keyattr_dynamic=False)
137+
b.a = 2
138+
b.b = 3
139+
with self.assertRaises(AttributeError):
140+
b.c.d = 3
82141

83142
def test_setitem_with_non_existing_key(self):
84-
b = benedict(keyattr_enabled=True)
143+
b = benedict(keyattr_enabled=True, keyattr_dynamic=True)
144+
b.a = "ok"
145+
self.assertEqual(b.a, "ok")
146+
147+
def test_setitem_with_non_existing_key_not_dynamic(self):
148+
b = benedict(keyattr_enabled=True, keyattr_dynamic=False)
85149
b.a = "ok"
86150
self.assertEqual(b.a, "ok")
87151

88152
def test_setitem_with_non_existing_nested_keys(self):
89-
b = benedict(keyattr_enabled=True)
153+
b = benedict(keyattr_enabled=True, keyattr_dynamic=True)
90154
b.a.b.c = "ok"
91-
self.assertEqual(
92-
b,
93-
{
94-
"a": {
95-
"b": {
96-
"c": "ok",
97-
},
98-
},
99-
},
100-
)
155+
self.assertEqual(b.a.b.c, "ok")
156+
157+
def test_setitem_with_non_existing_nested_keys_not_dynamic(self):
158+
b = benedict(keyattr_enabled=True, keyattr_dynamic=False)
159+
with self.assertRaises(AttributeError):
160+
b.a.b.c = "ok"
101161

102162
def test_setitem_with_conflicting_key(self):
103163
d = {
@@ -111,15 +171,48 @@ def test_keyattr_enabled_default(self):
111171
d = benedict()
112172
self.assertTrue(d.keyattr_enabled)
113173

174+
def test_keyattr_dynamic_default(self):
175+
d = benedict()
176+
self.assertFalse(d.keyattr_dynamic)
177+
114178
def test_keyattr_enabled_from_constructor(self):
115179
d = benedict(keyattr_enabled=True)
116180
self.assertTrue(d.keyattr_enabled)
117181
d = benedict(keyattr_enabled=False)
118182
self.assertFalse(d.keyattr_enabled)
119183

184+
def test_keyattr_dynamic_from_constructor(self):
185+
d = benedict(keyattr_dynamic=True)
186+
self.assertTrue(d.keyattr_dynamic)
187+
d = benedict(keyattr_dynamic=False)
188+
self.assertFalse(d.keyattr_dynamic)
189+
120190
def test_keyattr_enabled_getter_setter(self):
121191
d = benedict()
122192
d.keyattr_enabled = False
123193
self.assertFalse(d.keyattr_enabled)
124194
d.keyattr_enabled = True
125195
self.assertTrue(d.keyattr_enabled)
196+
197+
def test_keyattr_dynamic_getter_setter(self):
198+
d = benedict()
199+
d.keyattr_dynamic = False
200+
self.assertFalse(d.keyattr_dynamic)
201+
d.keyattr_dynamic = True
202+
self.assertTrue(d.keyattr_dynamic)
203+
204+
def test_keyattr_enabled_inheritance_on_casting(self):
205+
d = benedict(keyattr_enabled=True)
206+
c = benedict(d)
207+
self.assertTrue(c.keyattr_enabled)
208+
d = benedict(keyattr_enabled=False)
209+
c = benedict(d)
210+
self.assertFalse(c.keyattr_enabled)
211+
212+
def test_keyattr_dynamic_inheritance_on_casting(self):
213+
d = benedict(keyattr_dynamic=True)
214+
c = benedict(d)
215+
self.assertTrue(c.keyattr_dynamic)
216+
d = benedict(keyattr_dynamic=False)
217+
c = benedict(d)
218+
self.assertFalse(c.keyattr_dynamic)

0 commit comments

Comments
 (0)