Commit ec36791b authored by Janos's avatar Janos 🕴
Browse files

Merge branch 'add-parenthesis-operator' into 'main'

add ()-operator for safe dict traversal

See merge request !3
parents ab657379 0b53ec66
Pipeline #504309104 passed with stages
in 4 minutes and 50 seconds
......@@ -19,6 +19,7 @@ wrapped into an `Adict` when they're returned.
- Default `dict` behavior
- Full wrapping of nested `dict`s
- Fail-safe attribute notation (`adict.key`) doesn't raise `KeyError`
- Save traversing using parenthesis syntax (`('key')`)
- Supports nested dicts
- Supports JSON encoding
......@@ -54,6 +55,28 @@ print(dict3.noob) # "me", attribute notation
print(dict3.sub.you) # "noob', nested attribute notation
```
## Safe traversing using paranthesis syntax
At the cost of not having `None` values, the `()` operator allows key access, which always returns
a valid (empty) `Adict` instance when the key doesn't exist. This allowes to traverse `dict`s
into depper levels, without intermediate `None` checks. This syntax is basically an abbreviation
of the `dict.get(key, default)` function, but has the additional feature to again wrap default
`dict` values into `Adict(dict)` results.
```py
dd = Adict({'noob': 'me', 'sub': {'you': 'noob'}})
print(dd('sub')('you')) # "noob"
# is equivalent to
print(dd('sub', {})('you', {}))
# is equiivalent to
print(dd.get('sub', {}).get('you', {}))
print(dd('nokey')) # {} (isinstance Adict)
print(dd('nokey', {})) # {} (isinstance Adict)
print(dd('nokey', None) # None
```
## JSON encoding
```py
......
......@@ -7,6 +7,9 @@ import json
from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING
__NONE__ = object()
# only py>=39 supports correct type hinting MutableMapping[Any, Any]
class Adict(MutableMapping): # type: ignore
"""Attribute-accessible implementation of a dictionary."""
......@@ -99,6 +102,18 @@ class Adict(MutableMapping): # type: ignore
"""Dict representation is the contained dictonary."""
return self.__dict
# safe accessing
def __call__(self, key: str, default: Any = __NONE__) -> Any:
value = self.__getattr__(key)
if value is not None:
return value
if default is not __NONE__:
if isinstance(default, dict):
return Adict(default)
return default
return Adict()
class JsonEncoder(json.encoder.JSONEncoder):
"""Basic Json Encoder, which transforms any Adict elements into back into their __dict__
......
......@@ -2,7 +2,7 @@ import setuptools
setuptools.setup(
name='dictat',
version='1.0.0',
version='1.1.0',
description='Adict is an attribute-accessible dynamic dictionary wrapper',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
......
......@@ -115,6 +115,42 @@ def test_sub_dict(adict: Adict, subdict: Dict[Any, Any]) -> None:
assert adict.sub.subsub.subkey2 == 'subval2'
def test_paren_syntax(adict: Adict, subdict: Dict[Any, Any]) -> None:
"""Test safe access to non-existing keys using the paranthesis syntax.
The only thing that can raise from here, is if we've traversed down to a leaf value, which
then cannot accessed using () syntax."""
adict.sub = subdict
assert adict('sub')('key1') == 'val1'
assert adict('sub')('key2') == 'val2'
assert adict('sub')('subsub')('subkey1') == 'subval1'
assert adict('sub')('subsub')('subkey2') == 'subval2'
assert not adict('a')('b')('c')
assert adict('a')('b')('c') == {}
assert not adict('sub')('key3')
assert adict('sub')('key3') == {}
# with default value, e.g. when we know the last level is a leaf level
assert adict('a')('b')('c', default=None) is None
assert adict('sub')('key3', default=None) is None
assert adict('a')('b')('c', 'd') == 'd'
# test that the default=__NONE__=object() semantic is correct
assert adict('a')('b')('c', default=object()) is not None
assert adict('a')('b')('c', default=object()) != {}
# equivalence of `dict.get('key', {})`, but converting `{}` to `Adict({})``
assert adict('a', default={}) == {}
assert isinstance(adict('a', default={}), Adict)
assert adict('a', default={'b': 'c'})('b') == 'c'
# the only chance to raise is when we ()-access a leaf value
with pytest.raises(TypeError) as info:
adict('sub')('subsub')('subkey2')('nomore')
assert "'str' object is not callable" in str(info)
def test_delete(adict: Adict) -> None:
# non-existing
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment