Last active
September 29, 2022 00:38
-
-
Save aayla-secura/7e619ab46f6e1afc1087285e60b42af7 to your computer and use it in GitHub Desktop.
A magic dictionary which never raises KeyError, can set default values for keys based on regex and can filter based on regex
This file contains hidden or 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
# EXAMPLE USAGE | |
# import json | |
# | |
# mdorder = MagicDict() | |
# mdorder.configure( | |
# defaults={'^price$': 0, '_address$': 'No such street, PO 000'}) | |
# create a default order | |
# mdorder['price'] | |
# mdorder['shipping_address'] | |
# mdorder['billing_address'] | |
# | |
# md = MagicDict() | |
# md.configure( | |
# defaults={'^[0-9]+$': mdorder}, filter_re='^!') | |
# md['customers']['foo']['order'][1]['price'] = 15 | |
# md['customers']['foo']['order'][1]['shipping_address'] = 'FOO street' | |
# md['customers']['foo']['order'][1]['billing_address'] = 'FOO office' | |
# md['customers']['foo']['order'][1]['!notes'] = 'important notes' | |
# md['customers']['foo']['order'][2]['price'] = 25 # use default address | |
# md['customers']['!important customer']['order'][1] # use all defaults | |
# print(json.dumps(md, default=lambda o: o.data, indent=2)) | |
# print('\n-----important only:-----\n') | |
# print(json.dumps(md.filter(), default=lambda o: o.data, indent=2)) | |
# | |
# | |
# $ python3 magicdict.py | |
# { | |
# "customers": { | |
# "foo": { | |
# "order": { | |
# "1": { | |
# "price": 15, | |
# "shipping_address": "FOO street", | |
# "billing_address": "FOO office", | |
# "!notes": "important notes" | |
# }, | |
# "2": { | |
# "price": 25, | |
# "shipping_address": "No such street, PO 000", | |
# "billing_address": "No such street, PO 000" | |
# } | |
# } | |
# }, | |
# "!important customer": { | |
# "order": { | |
# "1": { | |
# "price": 0, | |
# "shipping_address": "No such street, PO 000", | |
# "billing_address": "No such street, PO 000" | |
# } | |
# } | |
# } | |
# } | |
# } | |
# | |
# -----important only:----- | |
# | |
# { | |
# "customers": { | |
# "foo": { | |
# "order": { | |
# "1": { | |
# "!notes": "important notes" | |
# } | |
# } | |
# }, | |
# "!important customer": { | |
# "order": { | |
# "1": { | |
# "price": 0, | |
# "shipping_address": "No such street, PO 000", | |
# "billing_address": "No such street, PO 000" | |
# } | |
# } | |
# } | |
# } | |
# } | |
import re | |
import logging | |
from copy import deepcopy | |
from collections import UserDict | |
from collections.abc import MutableMapping | |
logger = logging.getLogger(__name__) | |
def is_str_like(val): | |
return isinstance(val, (str, bytes, int, float, bool)) | |
class MagicDict(UserDict): | |
'''Never raises KeyError, sets the requested key if missing | |
The configure method sets the configuration. Default one is: | |
_conf = {'defaults': {}, 'filter_re': None} | |
- filter_re sets the regex used by filter | |
- defaults is a dictionary of regex--value pairs; the value is | |
returned whenever a missing key matching the regex is accessed. | |
If a missing key doesn't match any defined default, then a new | |
MagicDict is returned (and the configuration is copied to it). | |
''' | |
_default_conf = {'defaults': {}, 'filter_re': None} | |
def __init__(self, *args, **kargs): | |
super().__init__(*args, **kargs) | |
@property | |
def conf(self): | |
try: | |
return self._conf | |
except AttributeError: | |
try: | |
p = self._parent | |
except AttributeError: | |
self._conf = deepcopy(self.__class__._default_conf) | |
else: | |
self._conf = deepcopy(p.conf) | |
return self._conf | |
def _new_child(self, *args, **kargs): | |
child = self.__class__(*args, **kargs) | |
child._parent = self | |
return child | |
def __setitem__(self, key, val): | |
if isinstance(val, MutableMapping): | |
md = self._new_child(val) | |
return super().__setitem__(key, md) | |
if not is_str_like(key): | |
raise TypeError( | |
('{} works with keys which are strings and numbers ' | |
'as others may not convert to a srting for regex ' | |
'matching in an expected way.'.format( | |
self.__class__.__name__))) | |
return super().__setitem__(key, val) | |
def __getitem__(self, key): | |
if not is_str_like(key): | |
raise TypeError( | |
('{} works with keys which are strings and numbers ' | |
'as others may not convert to a srting for regex ' | |
'matching in an expected way.'.format( | |
self.__class__.__name__))) | |
try: | |
return super().__getitem__(key) | |
except KeyError: | |
for regex, val in self.conf['defaults'].items(): | |
if re.search(regex, str(key)): | |
self[key] = val | |
return self[key] | |
self[key] = {} # will be converted to MagicDict | |
return self[key] | |
def filter(self): | |
'''Filters dictionary based on the filter''' | |
if self.conf['filter_re'] is None: | |
return self | |
def _filter(md, empty_is_None=False): | |
# create a new empty dictionary of the same parent as md | |
try: | |
p = md._parent | |
except AttributeError: | |
res = md.__class__() | |
else: | |
res = p._new_child() | |
for k, v in md.items(): | |
if re.search(md.conf['filter_re'], str(k)): | |
res[k] = v | |
elif isinstance(v, MagicDict): | |
f = _filter(v, empty_is_None=True) | |
if f is not None: | |
res[k] = f | |
elif re.search(md.conf['filter_re'], str(v)): | |
res[k] = v | |
if not res and empty_is_None: | |
return None | |
return res | |
return _filter(self) | |
def configure(self, conf=None, **kargs): | |
'''Updates the configuration''' | |
_conf = conf | |
if _conf is None: | |
_conf = kargs | |
self.conf.update(deepcopy(_conf)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is now on Pypi: https://pypi.org/project/awesomedict/