Skip to content

Instantly share code, notes, and snippets.

@aayla-secura
Last active September 29, 2022 00:38
Show Gist options
  • Save aayla-secura/7e619ab46f6e1afc1087285e60b42af7 to your computer and use it in GitHub Desktop.
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
# 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))
@aayla-secura
Copy link
Author

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