Source code for libb.attrdict

from abc import ABCMeta
from collections.abc import Mapping, MutableMapping

__all__ = [
    'attrdict',
    'lazydict',
    'emptydict',
    'bidict',
    'MutableDict',
    'CaseInsensitiveDict',
]


[docs] class attrdict(dict): """A dictionary subclass that allows attribute-style access. This is a dictionary that allows access to its keys as attributes (using dot notation) in addition to standard dictionary access methods. Basic Usage:: >>> import copy >>> d = attrdict(x=10, y='foo') >>> d.x 10 >>> d['x'] 10 >>> d.y = 'baa' >>> d['y'] 'baa' >>> g = d.copy() >>> g.x = 11 >>> d.x 10 >>> d.z = 1 >>> d.z 1 >>> 'x' in d True >>> 'w' in d False >>> d.get('x') 10 >>> d.get('w') Deep Copy Behavior:: >>> tricky = [d, g] >>> tricky2 = copy.copy(tricky) >>> tricky2[1].x 11 >>> tricky2[1].x = 12 >>> tricky[1].x 12 >>> righty = copy.deepcopy(tricky) >>> righty[1].x 12 >>> righty[1].x = 13 >>> tricky[1].x 12 Access Methods (handles ``obj.get('attr')``, ``obj['attr']``, and ``obj.attr``):: >>> class A(attrdict): ... @property ... def x(self): ... return 1 >>> a = A() >>> a['x'] == a.x == a.get('x') True >>> a.get('b') >>> a['b'] Traceback (most recent call last): ... KeyError: 'b' >>> a.b Traceback (most recent call last): ... AttributeError: b A method name is not data, so the fallback stops before the names this class defines. Store the key to read one:: >>> a['keys'] Traceback (most recent call last): ... KeyError: 'keys' >>> attrdict(keys=3)['keys'] 3 """ __slots__ = () def __getattr__(self, attrname): try: return dict.__getitem__(self, attrname) except KeyError: raise AttributeError(attrname) from None def __setattr__(self, attrname, attrval): if isinstance(attrval, ABCMeta): dict.__setattr__(self, attrname, attrval) else: self[attrname] = attrval def __delattr__(self, attrname): try: self.pop(attrname) except KeyError: raise AttributeError(attrname) from None
[docs] def __getitem__(self, attrname): """Read a key, or a subclass attribute of that name. Parameters ---------- attrname : str Key to read. A stored key always wins. Returns ------- Any The stored value, else the attribute of that name. Raises ------ KeyError The name is neither a key, nor an attribute the subclass declares. Notes ----- - The attribute fallback is what makes a declared property read the same three ways: `obj['x']`, `obj.x` and `obj.get('x')`. - It stops at this class's own method names, which are data to nobody. That stop is what keeps `name in obj`, `obj[name]` and `obj.get(name, default)` answering alike for every name. """ try: return dict.__getitem__(self, attrname) except KeyError: pass if attrname in _MAPPING_API: raise KeyError(attrname) try: return dict.__getattribute__(self, attrname) except AttributeError: raise KeyError(attrname) from None
[docs] def get(self, key, default=None): try: return self.__getitem__(key) except KeyError: return default
[docs] def update(self, *args, **kwargs): dict.update(self, *args, **kwargs) return self
[docs] def copy(self, **kwargs): """Shallow-copy into the same class, then apply `kwargs` as overrides. Parameters ---------- **kwargs Keys written over the copy after it is made. Returns ------- attrdict A new instance of `type(self)`, never of `attrdict` itself. Notes ----- - The class carries the read behavior, so a copy has to keep it: a `lazydict` copied into an `attrdict` hands back a stored callable instead of resolving it. """ newdict = self.__class__(dict.copy(self)) return newdict.update(**kwargs)
[docs] @classmethod def fromkeys(cls, iterable, value=None): """Create a new attrdict with keys from iterable and values set to value. """ return cls(dict.fromkeys(iterable, value))
[docs] def __or__(self, other): """Merge with `other` on the right, keeping this class. Returns ------- attrdict A new instance of `type(self)`, with `other`'s values winning a shared key. Notes ----- - The class is kept for the reason `copy` gives. """ return self.__class__(dict.__or__(self, other))
[docs] def __ror__(self, other): """Merge with `other` on the left, keeping this class. Returns ------- attrdict A new instance of `type(self)`, with this mapping's values winning a shared key, since it is the right operand. """ return self.__class__(dict.__ror__(self, other))
def __ior__(self, other): dict.update(self, other) return self
_MAPPING_API = frozenset(name for name in dir(attrdict) if not name.startswith('_'))
[docs] class lazydict(attrdict): """A dictionary where function values are lazily evaluated. Functions stored as values are called with the dictionary as argument when the key is accessed, making them behave like computed properties. Basic Usage:: >>> a = lazydict(a=1, b=2, c=lambda x: x.a+x.b) >>> a.c 3 >>> a.a = 99 >>> a.c # Recalculated with new value 101 >>> a.z = 1 >>> a.z 1 Instance Isolation (descriptors are not shared between instances):: >>> z = lazydict(a=2, y=4, f=lambda x: x.a*x.y) >>> z.b Traceback (most recent call last): ... AttributeError: b >>> z.c Traceback (most recent call last): ... AttributeError: c >>> z.f 8 >>> a.f Traceback (most recent call last): ... AttributeError: f """ __slots__ = () def __getattr__(self, attrname): try: attrval = dict.__getitem__(self, attrname) except KeyError: raise AttributeError(attrname) from None if callable(attrval): return attrval(self) return attrval
[docs] class emptydict(attrdict): """A dictionary that returns None for non-existing keys without raising exceptions. Similar to attrdict but returns None instead of raising KeyError or AttributeError when accessing non-existing keys. Basic Usage:: >>> a = emptydict(a=1, b=2) >>> a.c == None True >>> a.b 2 >>> a['b'] 2 >>> 'c' in a False >>> 'b' in a True >>> a.get('b') 2 >>> a.get('c') """ __slots__ = () def __getattr__(self, attrname): try: return attrdict.__getattr__(self, attrname) except AttributeError: return def __getitem__(self, attrname): try: return attrdict.__getitem__(self, attrname) except KeyError: return
[docs] class bidict(dict): """Bidirectional dictionary that allows multiple keys with the same value. Maintains an inverse mapping from values to lists of keys that enables bidirectional lookup. Basic Usage:: >>> bd = bidict({'a': 1, 'b': 2}) >>> bd {'a': 1, 'b': 2} >>> bd.inverse {1: ['a'], 2: ['b']} Multiple Keys with Same Value:: >>> bd['c'] = 1 >>> bd {'a': 1, 'b': 2, 'c': 1} >>> bd.inverse {1: ['a', 'c'], 2: ['b']} Removing Keys Updates Inverse:: >>> del bd['c'] >>> bd {'a': 1, 'b': 2} >>> bd.inverse {1: ['a'], 2: ['b']} >>> del bd['a'] >>> bd {'b': 2} >>> bd.inverse {2: ['b']} Changing Values Updates Inverse:: >>> bd['b'] = 3 >>> bd {'b': 3} >>> bd.inverse {2: [], 3: ['b']} """ __slots__ = ('inverse',) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.inverse = {} for key, value in list(self.items()): self.inverse.setdefault(value, []).append(key) def __setitem__(self, key, value): if key in self: self.inverse[self[key]].remove(key) super().__setitem__(key, value) self.inverse.setdefault(value, []).append(key) def __delitem__(self, key): self.inverse.setdefault(self[key], []).remove(key) if self[key] in self.inverse and not self.inverse[self[key]]: del self.inverse[self[key]] super().__delitem__(key)
[docs] class MutableDict(dict): """An ordered dictionary with additional insertion methods. Extends the standard dictionary with methods to insert items before or after existing keys. Relies on Python 3.7+ dictionary implementation which preserves insertion order. """ __slots__ = ()
[docs] def insert_before(self, key, new_key, val): """Insert new_key:value into dict before key. Example:: >>> d = MutableDict({'a': 1, 'b': 2, 'c': 3}) >>> d.insert_before('b', 'x', 10) >>> list(d.keys()) ['a', 'x', 'b', 'c'] >>> d['x'] 10 """ keys = list(self.keys()) vals = list(self.values()) insert_idx = keys.index(key) keys.insert(insert_idx, new_key) vals.insert(insert_idx, val) self.clear() self.update({x: vals[i] for i, x in enumerate(keys)})
[docs] def insert_after(self, key, new_key, val): """Insert new_key:value into dict after key. Example:: >>> d = MutableDict({'a': 1, 'b': 2, 'c': 3}) >>> d.insert_after('a', 'x', 10) >>> list(d.keys()) ['a', 'x', 'b', 'c'] >>> d['x'] 10 """ keys = list(self.keys()) vals = list(self.values()) insert_idx = keys.index(key) + 1 if keys[-1] != key: keys.insert(insert_idx, new_key) vals.insert(insert_idx, val) self.clear() self.update({x: vals[i] for i, x in enumerate(keys)}) else: self.update({new_key: val})
[docs] class CaseInsensitiveDict(MutableMapping): """A case-insensitive dictionary-like object. Implements all methods and operations of ``MutableMapping`` as well as dict's ``copy``. Also provides ``lower_items``. All keys are expected to be strings. The structure remembers the case of the last key to be set, and ``iter(instance)``, ``keys()``, ``items()`` will contain case-sensitive keys. However, querying and contains testing is case insensitive: cid = CaseInsensitiveDict() cid['Accept'] = 'application/json' cid['aCCEPT'] == 'application/json' # True list(cid) == ['Accept'] # True For example, ``headers['content-encoding']`` will return the value of a ``'Content-Encoding'`` response header, regardless of how the header name was originally stored. Note: If the constructor, ``.update``, or equality comparison operations are given keys that have equal ``.lower()`` values, the behavior is undefined. """ __slots__ = ('_store',) def __init__(self, data=None, **kwargs): self._store = {} if data is None: data = {} self.update(data, **kwargs) def __setitem__(self, key, value): self._store[key.lower()] = (key, value) def __getitem__(self, key): return self._store[key.lower()][1] def __delitem__(self, key): del self._store[key.lower()] def __iter__(self): return (casedkey for casedkey, mappedvalue in self._store.values()) def __len__(self): return len(self._store)
[docs] def lower_items(self): """Like iteritems(), but with all lowercase keys. Example:: >>> cid = CaseInsensitiveDict({'Content-Type': 'application/json'}) >>> list(cid.lower_items()) [('content-type', 'application/json')] """ return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
def __eq__(self, other): if isinstance(other, Mapping): other = CaseInsensitiveDict(other) else: return NotImplemented return dict(self.lower_items()) == dict(other.lower_items())
[docs] def copy(self): return CaseInsensitiveDict(self._store.values())
def __repr__(self): return str(dict(self.items()))
if __name__ == '__main__': __import__('doctest').testmod(optionflags=4 | 8 | 32)