Source code for qnd.adict

"""An items-as-attributes dict."""
from __future__ import absolute_import

import sys
# from collections import Mapping

PY2 = sys.version_info < (3,)


[docs]class ItemsAreAttrs(object): """Mix-in class for QArray, QGroup, or QList, and also ADict.""" __slots__ = () def __getattr__(self, name): # Mixing __getattr__ with properties, as we do here, can lead to # very obscure errors, because the way __getattr__ works is to # simply try to retrieve the attribute, and call __getattr__ if # that operation raises an exception, instead of propagating the # exception as usual. When you have a property or any descriptor, # the act of retrieving that attribute implicitly calls a method. # If that property method raises an exception, the __getattr__ # machinery interprets that to mean the attribute does not exist # and calls __getattr__, removing the actual faulting method from # the call chain and making debugging difficult. # Beware of errors with this __getattr__ in their calling chain; # they may have originated in a different error in a property # method! if name.startswith('__') and len(name) > 2 or name == 'getdoc': # Do not redirect dunder or ipython getdoc calls, as this # confuses many simple attempts at introspection. return super(ItemsAreAttrs, self).__getattr__(name) # Strip single trailing _ as an interactive convenience for the # problem of attribute names that match reserved words or property # or method names. This is inspired by the PEP8 advice for dealing # with this issue. if name.endswith('_'): name = name[:-1] try: return self[name] except KeyError as e: raise AttributeError(*e.args) def __setattr__(self, name, value): if name.startswith('__') and len(name) > 2: super(ItemsAreAttrs, self).__setattr__(name, value) if name.endswith('_'): name = name[:-1] self[name] = value def __delattr__(self, name): if name.startswith('__') and len(name) > 2: super(ItemsAreAttrs, self).__delattr__(name) if name.endswith('_'): name = name[:-1] del self[name]
[docs] def update(self, *args, **kwargs): """Multiple __setitem__ from positional arguments or keywords.""" for arg in args: if hasattr(arg, 'keys'): # dict-like, not list-like for key in arg: self[key] = arg[key] else: key, value = arg self[key] = value for key in kwargs: self[key] = kwargs[key[:-1] if key.endswith('_') else key]
[docs]class ADict(ItemsAreAttrs, dict): """Subclass of dict permitting access to items as if they were attributes. For a ADict ad, ``ad.x`` is equivalent to ``ad['x']`` for getting, setting, or deleting items. The exceptions are dict method names, like `keys` or `items`, syntactically illegal names, like `class` or `yield`, and any name beginning with `__`. Additionally, as a work around for some of these exceptions, ADict will remove a single trailing underscore from an attribute name, so ``ad.x_`` is also equivalent to ``ad['x']``, and you need ``ad.x__`` to get ``ad['x_']`` (a convention inspired by the similar PEP8 recommendation for syntatically illegal variable names). The trailing underscore removal does not apply to names beginning with `__`. The trailing underscore removal convention applies to keywords passed to the constructor or to the `update` method as well. Use subscript syntax when a variable or expression holds an item name; use attribute syntax when you know the item name at parse time:: ad[variable] = value # value of variable is the item name ad.fixed = value # 'fixed' is the item name value = ad.get('fixed', default) # except to avoid KeyError See Also -------- redict : recursively toggle between dict and ADict ItemsAreAttrs : mixin base class to provide this for any class """ __slots__ = () def __init__(self, *args, **kwargs): super(ADict, self).__init__(*args) self.update(kwargs) def __repr__(self): return "ADict(" + super(ADict, self).__repr__() + ")"
[docs]def redict(d, cls=None): """Recursively convert a nested dict to a nested ADict and vice versa. Parameters ---------- d : dict or ADict instance A dict, possibly nested, to be converted. cls : dict or subclass of dict, optional The dict-like cls to recursively convert `d` and any sub-dicts into. By default, if `d` is a `ADict`, `cls` is `dict`, otherwise `cls` is `ADict`, so repeated calls to `redict` toggle between `dict` and `ADict`. Returns ------- dnew : dict or ADict A copy of `d` whose class is `cls`. Any items which are dict instances are similarly copied to be `cls` instances. Non-dict items are not copied unless assignment makes copies. """ if cls is None: cls = dict if isinstance(d, ADict) else ADict dnew = cls(d) for key, value in (d.iteritems() if PY2 else d.items()): if hasattr(value, '__iter__') and hasattr(value, 'keys'): dnew[key] = redict(value, cls) return dnew