Created
April 14, 2016 02:27
-
-
Save metaist/13640b4ea7cdb3fa501b86a709b22e5e to your computer and use it in GitHub Desktop.
Manage flexible dicts in python.
This file contains 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
#!/usr/bin/env python | |
# coding: utf-8 | |
'''Manage namespaces.''' | |
# Native | |
import collections | |
# <dict> shorthand | |
_del = dict.__delitem__ | |
_get = dict.__getitem__ | |
_in = dict.__contains__ | |
_set = dict.__setitem__ | |
_getattr = object.__getattribute__ | |
_setattr = object.__setattr__ | |
def ismap(item): | |
'''Return True if item is a mapping. | |
Args: | |
item (any): item to check | |
Returns: | |
(bool): True if item is a Mapping, False otherwise. | |
''' | |
return isinstance(item, collections.Mapping) | |
def byteify(item): | |
'''Return str instead of unicode. | |
Args: | |
item (any): item to encode | |
Returns: | |
(any): all of the unicode items replaced with strings | |
Examples: | |
>>> byteify(u'test') == 'test' | |
True | |
>>> byteify([u'a', u'b', 'c']) == ['a', 'b', 'c'] | |
True | |
>>> byteify({u'd': {u'e': 'f'}}) == {'d': {'e': 'f'}} | |
True | |
''' | |
if ismap(item): | |
return {byteify(key): byteify(value) | |
for key, value in item.iteritems()} | |
elif isinstance(item, list): | |
return [byteify(element) for element in item] | |
elif isinstance(item, unicode): | |
return item.encode('utf-8') | |
else: | |
return item | |
def dictify(item): | |
'''Return a dict version of a Mapping. | |
Args: | |
item (collections.Mapping): item to convert | |
Result: | |
(dict): all deeper namespaces converted to dicts | |
''' | |
result = {} | |
for k, v in item.items(): | |
if isinstance(v, NS): | |
result[k] = dictify(v) | |
else: | |
result[k] = v | |
return result | |
def namespaceify(item, delimeter=':'): | |
'''Return a namespaced version of a Mapping. | |
Args: | |
item (collections.Mapping): item to convert | |
delimeter (str): namespace delimeter (default: ':') | |
Returns: | |
(NS): namespaced version of item | |
''' | |
result = NS() | |
result.delimeter = delimeter | |
for k, v in item.items(): | |
if not isinstance(v, NS) and ismap(v): | |
result[k] = namespaceify(v, delimeter) | |
else: | |
result[k] = v | |
return result | |
def merge(first, *others): | |
'''Return a dictionary in which the keys are merged. | |
Args: | |
first (dict): the initial dictionary to merge | |
*others (dict...): additional dictionaries to merge | |
Return: | |
(dict): the results of the merge | |
''' | |
result = first | |
for item in others: | |
if not item: | |
continue | |
for key, next_val in item.items(): | |
prev_val = result.get(key) | |
prev_type, next_type = type(prev_val), type(next_val) | |
if prev_type == next_type and prev_val == next_val: | |
continue # already set to the correct value / type | |
if ismap(prev_val) and ismap(next_val): | |
result[key] = merge(prev_val, next_val) | |
else: | |
result[key] = next_val | |
return result | |
class AttrDict(dict): | |
'''Flexible dict with attribute access.''' | |
def __setattr__(self, name, value): | |
'''Set the value of an attribute. | |
Args: | |
name (str): name of the attribute | |
value (any): value to set | |
Examples: | |
>>> item = AttrDict() | |
>>> item.a = 1 | |
>>> item['a'] == 1 | |
True | |
''' | |
try: | |
_getattr(self, name) | |
_setattr(self, name, value) | |
except AttributeError: | |
self[name] = value | |
def __getattr__(self, name): | |
'''Return the value of the attribute. | |
Args: | |
name (str): name of the attribute | |
Returns: | |
(any): value of the attribute, or None if it is missing | |
Examples: | |
>>> item = AttrDict(a=1) | |
>>> item.a == 1 | |
True | |
''' | |
return self[name] | |
def __delattr__(self, name): | |
'''Delete the attribute. | |
Args: | |
name (str): name of the attribute | |
Examples: | |
>>> item = AttrDict(a=1, b=2) | |
>>> del item.a | |
>>> item == {'b': 2} | |
True | |
''' | |
del self[name] | |
def __setitem__(self, name, value): | |
'''Set the value of a key. | |
Args: | |
name (str): name of the key | |
value (any): value to set | |
Examples: | |
>>> item = AttrDict() | |
>>> item['a'] = 1 | |
>>> item.a == 1 | |
True | |
''' | |
_set(self, name, value) | |
def __getitem__(self, name): | |
'''Return the value of the key. | |
Args: | |
name (str): name of the key | |
Returns: | |
(any): value of the key, or None if it is missing | |
Examples: | |
>>> item = AttrDict(a=1) | |
>>> item['a'] == 1 | |
True | |
''' | |
result = None | |
if _in(self, name): | |
result = _get(self, name) | |
return result | |
def __delitem__(self, name): | |
'''Delete a key. | |
Args: | |
name (str): name of the key | |
Examples: | |
>>> item = AttrDict(a=1, b=2) | |
>>> del item['a'] | |
>>> item == {'b': 2} | |
True | |
''' | |
_del(self, name) | |
def __iadd__(self, other): | |
'''Add another object to this object in place. | |
Args: | |
other (collections.Mapping): object to add | |
Examples: | |
>>> item = AttrDict(a=1) | |
>>> item += {'b': {'c': 3}} | |
>>> item == {'a': 1, 'b': {'c': 3}} | |
True | |
''' | |
merge(self, other) | |
return self | |
def __add__(self, other): | |
'''Add another object to this object. | |
Args: | |
other (collections.Mapping): object to add | |
Returns: | |
(AttrDict): new instance | |
Examples: | |
>>> item = AttrDict(a=1) + {'b': 2} | |
>>> item == AttrDict(a=1, b=2) | |
True | |
''' | |
return AttrDict(merge({}, self, other)) | |
def __radd__(self, other): | |
'''Add this object to another object. | |
Returns: | |
(AttrDict): new instance | |
Args: | |
other (collections.Mapping): object to add | |
Examples: | |
>>> item = {'b': 2} + AttrDict(a=1) | |
>>> item == AttrDict(a=1, b=2) | |
True | |
''' | |
return AttrDict(merge({}, other, self)) | |
class NS(AttrDict): | |
'''A general-purpose dict-like namespace.''' | |
delimeter = ':' | |
def __init__(self, *args, **kwds): | |
'''Construct a namespace from parameters. | |
Args: | |
*args: dictionaries to merge | |
**kwds: keyword arguments to convert into a dictionary | |
Examples: | |
>>> ns = NS() | |
>>> ns == {} | |
True | |
>>> ns = NS({'a': 1}, {'b': 2}, {'c': 3}) | |
>>> ns == {'a': 1, 'b': 2, 'c': 3} | |
True | |
''' | |
# pylint: disable=super-init-not-called | |
args = list(args) | |
args.append(kwds) | |
for arg in args: | |
if not arg: # ignore empty | |
continue | |
if not ismap(arg): | |
raise TypeError('[{0}] cannot be merged'.format(arg)) | |
merge(self, arg) | |
def __contains__(self, key): | |
'''Returns True if key is in the Namespace. | |
Args: | |
key (str): dotted name of the key | |
Returns: | |
(bool): True if the name is in the namespace; False otherwise | |
Examples: | |
>>> ns = NS(a=1, b=None, d={'e': {'f': 5}}) | |
>>> 'a' in ns | |
True | |
>>> 'b' in ns | |
False | |
>>> 'c' in ns | |
False | |
>>> 'd:e:f' in ns | |
True | |
''' | |
result = True | |
obj = self | |
parts = key.split(self.delimeter) | |
for part in parts: | |
if ismap(obj) and _in(obj, part): | |
obj = _get(obj, part) | |
else: | |
result = False | |
break | |
return result | |
def __delitem__(self, key): | |
'''Remove a key from the object. | |
Args: | |
key (str): key to remove | |
Examples: | |
>>> ns = NS(a={'b': 3}) | |
>>> ns['a:b'] == 3 | |
True | |
>>> del ns['a:b'] | |
>>> ns['a:b'] is None | |
True | |
''' | |
obj = self | |
parts = key.split(self.delimeter) | |
last = parts.pop() | |
for part in parts: | |
if ismap(obj) and _in(obj, part): | |
obj = _get(obj, part) | |
else: | |
raise KeyError(key) | |
_del(obj, last) | |
def __getitem__(self, key): | |
'''Return the value of the key. | |
Args: | |
key (str): dotted name of the key | |
Returns: | |
(any): value of the key, None if not found | |
''' | |
return self.get(key) | |
def get(self, key, default=None): | |
'''Return the value of the key. | |
Args: | |
key (str, list): dotted name of the key | |
default (any): value to return if key is not found (default: None) | |
Returns: | |
(any): value of the key | |
Examples: | |
>>> ns = NS(a={'b': {'c': 3}}) | |
>>> ns['a:b:c'] == 3 | |
True | |
>>> ns = NS() | |
>>> ns[u'a'] = {u'b': {u'c': 3}} | |
>>> ns[u'a:b:c'] == 3 | |
True | |
''' | |
obj = self | |
parts = key | |
if isinstance(key, basestring): | |
parts = key.split(self.delimeter) | |
for part in parts: | |
if ismap(obj) and _in(obj, part): | |
obj = _get(obj, part) | |
else: | |
return default | |
return obj | |
def __setitem__(self, key, value): | |
'''Set the value of the key. | |
Args: | |
key (str, list): dotted name of the key | |
value (any): value of the key | |
''' | |
self.set(key, value) | |
def set(self, key, value): | |
'''Set the value of the key. | |
Args: | |
key (str, list): dotted name of the key | |
value (any): value of the key | |
Returns: | |
(NS): self for chaining | |
Examples: | |
>>> ns = NS(a=1, b=2, c={'d': 4}) | |
>>> ns['a'] = 5 | |
>>> ns['a'] == 5 | |
True | |
>>> ns['c:d'] = 6 | |
>>> ns['c:d'] == 6 | |
True | |
>>> ns = NS() | |
>>> ns['a'] = {'b': {'c': 3}} | |
>>> ns['a:b:c'] == 3 | |
True | |
>>> isinstance(ns['a:b'], NS) | |
True | |
''' | |
parts = key | |
if isinstance(key, basestring): | |
parts = key.split(self.delimeter) | |
obj = self | |
last = parts.pop() | |
for part in parts: | |
if not _in(obj, part) or not ismap(_get(obj, part)): | |
_set(obj, part, NS()) | |
obj = _get(obj, part) | |
if ismap(value): | |
_set(obj, last, namespaceify(value, self.delimeter)) | |
else: | |
_set(obj, last, value) | |
return self |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment