#!/usr/bin/env python # -*- coding: utf-8 -*- """ Bunch is a subclass of dict with attribute-style access. >>> b = Bunch() >>> b.hello = 'world' >>> b.hello 'world' >>> b['hello'] += "!" >>> b.hello 'world!' >>> b.foo = Bunch(lol=True) >>> b.foo.lol True >>> b.foo is b['foo'] True It is safe to import * from this module: __all__ = ('Bunch', 'bunchify','unbunchify') un/bunchify provide dictionary conversion; Bunches can also be converted via Bunch.to/fromDict(). """ __version__ = '1.0.2' VERSION = tuple(map(int, __version__.split('.'))) __all__ = ('Bunch', 'bunchify', 'unbunchify',) from bunch.python3_compat import * class Bunch(dict): """ A dictionary that provides attribute-style access. >>> b = Bunch() >>> b.hello = 'world' >>> b.hello 'world' >>> b['hello'] += "!" >>> b.hello 'world!' >>> b.foo = Bunch(lol=True) >>> b.foo.lol True >>> b.foo is b['foo'] True A Bunch is a subclass of dict; it supports all the methods a dict does... >>> sorted(b.keys()) ['foo', 'hello'] Including update()... >>> b.update({ 'ponies': 'are pretty!' }, hello=42) >>> print (repr(b)) Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') As well as iteration... >>> sorted([ (k,b[k]) for k in b ]) [('foo', Bunch(lol=True)), ('hello', 42), ('ponies', 'are pretty!')] And "splats". >>> "The {knights} who say {ni}!".format(**Bunch(knights='lolcats', ni='can haz')) 'The lolcats who say can haz!' See unbunchify/Bunch.toDict, bunchify/Bunch.fromDict for notes about conversion. """ def __contains__(self, k): """ >>> b = Bunch(ponies='are pretty!') >>> 'ponies' in b True >>> 'foo' in b False >>> b['foo'] = 42 >>> 'foo' in b True >>> b.hello = 'hai' >>> 'hello' in b True >>> b[None] = 123 >>> None in b True >>> b[False] = 456 >>> False in b True """ return dict.__contains__(self, k) or hasattr(self, k) # only called if k not found in normal places def __getattr__(self, k): """ Gets key if it exists, otherwise throws AttributeError. nb. __getattr__ is only called if key is not found in normal places. >>> b = Bunch(bar='baz', lol={}) >>> b.foo Traceback (most recent call last): ... AttributeError: foo >>> b.bar 'baz' >>> getattr(b, 'bar') 'baz' >>> b['bar'] 'baz' >>> b.lol is b['lol'] True >>> b.lol is getattr(b, 'lol') True """ try: # Throws exception if not in prototype chain return object.__getattribute__(self, k) except AttributeError: try: return self[k] except KeyError: raise AttributeError(k) def __setattr__(self, k, v): """ Sets attribute k if it exists, otherwise sets key k. A KeyError raised by set-item (only likely if you subclass Bunch) will propagate as an AttributeError instead. >>> b = Bunch(foo='bar', this_is='useful when subclassing') >>> b.values #doctest: +ELLIPSIS >>> b.values = 'uh oh' >>> b.values 'uh oh' >>> b['values'] Traceback (most recent call last): ... KeyError: 'values' """ try: # Throws exception if not in prototype chain object.__getattribute__(self, k) except AttributeError: try: self[k] = v except KeyError: raise AttributeError(k) else: object.__setattr__(self, k, v) def __delattr__(self, k): """ Deletes attribute k if it exists, otherwise deletes key k. A KeyError raised by deleting the key--such as when the key is missing--will propagate as an AttributeError instead. >>> b = Bunch(lol=42) >>> del b.values # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: ...values... >>> del b.lol >>> b.lol Traceback (most recent call last): ... AttributeError: lol """ try: # Throws exception if not in prototype chain object.__getattribute__(self, k) except AttributeError: try: del self[k] except KeyError: raise AttributeError(k) else: object.__delattr__(self, k) def copy(self): """ Makes a shallow copy of the Bunch. >>> a = Bunch(foo={'bar': 'baz'}, hello=42) >>> b = a.copy() >>> b Bunch(foo={'bar': 'baz'}, hello=42) >>> a is b False >>> a.foo is b.foo True """ return self.__class__(self) def toDict(self, DictClass=dict): """ Recursively converts a Bunch back into a dictionary. >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') >>> b.toDict() == {'ponies': 'are pretty!', 'foo': {'lol': True}, 'hello': 42} True See unbunchify for more info. """ return unbunchify(self, DictClass) def __add__(self, other): """ Creates a shallow copy of the Bunch, merging in another Mapping (or Iterable of key-value pairs). >>> a = Bunch(foo={}, hello=42) >>> a + { 'lol': True } Bunch(foo={}, hello=42, lol=True) >>> { 'reversed': True } + a Bunch(foo={}, hello=42, reversed=True) >>> b = a + (('pairs', 'are fine'), ['lol', False]) >>> b Bunch(foo={}, hello=42, lol=False, pairs='are fine') >>> a is b False >>> a.foo is b.foo True """ b = self.copy() b.update(other) return b __radd__ = __add__ def __iadd__(self, other): """ Merges another Mapping (or Iterable of key-value pairs) into this Bunch. >>> a = Bunch(bar='baz', hello=0) >>> foo = { 'lol': True } >>> a += { 'hello': 42, 'foo': foo } >>> a Bunch(bar='baz', foo={'lol': True}, hello=42) >>> a.foo is foo True """ self.update(other) return self def __repr__(self, _repr_running={}): """ Invertible* string-form of a Bunch. >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') >>> print (repr(b)) Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') >>> eval(repr(b)) == b True (*) Invertible so long as collection contents are each repr-invertible. """ call_key = id(self), _get_ident() if call_key in _repr_running: return '...' _repr_running[call_key] = 1 try: if not self: return '%s()' % (self.__class__.__name__,) args = ', '.join( ('%s=%r' % (k, self[k]) for k in sorted(self)) ) return '%s(%s)' % (self.__class__.__name__, args) finally: del _repr_running[call_key] @classmethod def fromDict(cls, d): """ Recursively transforms a dictionary into a Bunch via copy. >>> b = Bunch.fromDict({'urmom': {'sez': {'what': 'what'}}}) >>> b.urmom.sez.what 'what' Aliased as ``Bunch.bunchify``. See ``bunch.bunchify`` for more info. """ return bunchify(d, cls) bunchify = fromDict # While we could convert abstract types like Mapping or Iterable, I think # bunchify is more likely to "do what you mean" if it is conservative about # casting (ex: isinstance(str,Iterable) == True ). # # Should you disagree, it is not difficult to duplicate this function with # more aggressive coercion to suit your own purposes. def bunchify(it, BunchClass=Bunch): """ Recursively transforms a dictionary into a Bunch via copy. >>> b = bunchify({'urmom': {'sez': {'what': 'what'}}}) >>> b.urmom.sez.what 'what' bunchify can handle intermediary dicts, lists and tuples (as well as their subclasses), but ymmv on custom datatypes. >>> b = bunchify({ 'lol': ('cats', {'hah':'i win again'}), ... 'hello': [{'french':'salut', 'german':'hallo'}] }) >>> b.hello[0].french 'salut' >>> b.lol[1].hah 'i win again' nb. As dicts are not hashable, they cannot be nested in sets/frozensets. You may customize Mapping conversion by passing a Bunch/dict class as the second parameter. """ if isinstance(it, Mapping): return BunchClass( (k, bunchify(it[k], BunchClass)) for k in iter(it) ) elif isinstance(it, (list, tuple)): return type(it)( (bunchify(v, BunchClass) for v in it) ) else: return it def unbunchify(it, DictClass=dict): """ Recursively converts a Bunch into a dictionary via copy. >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') >>> unbunchify(b) == {'ponies': 'are pretty!', 'foo': {'lol': True}, 'hello': 42} True unbunchify will handle intermediary dicts, lists and tuples (as well as their subclasses), but ymmv on custom datatypes. >>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42, ... ponies=('are pretty!', Bunch(lies='are trouble!'))) >>> unbunchify(b) == {'ponies': ('are pretty!', {'lies': 'are trouble!'}), ... 'foo': ['bar', {'lol': True}], 'hello': 42} True nb. As dicts are not hashable, they cannot be nested in sets/frozensets. You may customize Mapping conversion by passing a dict class as the second parameter. """ if isinstance(it, Mapping): return DictClass( (k, unbunchify(it[k], DictClass)) for k in iter(it) ) elif isinstance(it, (list, tuple)): return type(it)( (unbunchify(v, DictClass) for v in it) ) else: return it ### Serialization try: try: import json except ImportError: import simplejson as json def toJSON(self, **options): """ Serializes this Bunch to JSON. Accepts the same keyword options as ``json.dumps()``. >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') >>> json.dumps(b) '{"foo": {"lol": true}, "hello": 42, "ponies": "are pretty!"}' >>> b.toJSON() '{"foo": {"lol": true}, "hello": 42, "ponies": "are pretty!"}' """ return json.dumps(self, **options) Bunch.toJSON = toJSON except ImportError: pass try: # Attempt to register ourself with PyYAML as a representer import yaml from yaml.representer import Representer, SafeRepresenter def from_yaml(loader, node): """ PyYAML support for Bunches using the tag ``!bunch`` and ``!bunch.Bunch``. >>> import yaml >>> yaml.full_load(''' ... Flow style: !bunch.Bunch { Clark: Evans, Brian: Ingerson, Oren: Ben-Kiki } ... Block style: !bunch ... Clark : Evans ... Brian : Ingerson ... Oren : Ben-Kiki ... ''') #doctest: +NORMALIZE_WHITESPACE {'Flow style': Bunch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki'), 'Block style': Bunch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki')} This module registers itself automatically to cover both Bunch and any subclasses. Should you want to customize the representation of a subclass, simply register it with PyYAML yourself. """ data = Bunch() yield data value = loader.construct_mapping(node) data.update(value) def to_yaml_safe(dumper, data): """ Converts Bunch to a normal mapping node, making it appear as a dict in the YAML output. >>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42) >>> import yaml >>> yaml.safe_dump(b, default_flow_style=True) '{foo: [bar, {lol: true}], hello: 42}\\n' """ return dumper.represent_dict(data) def to_yaml(dumper, data): """ Converts Bunch to a representation node. >>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42) >>> import yaml >>> yaml.dump(b, default_flow_style=True) '!bunch.Bunch {foo: [bar, !bunch.Bunch {lol: true}], hello: 42}\\n' """ return dumper.represent_mapping(u('!bunch.Bunch'), data) yaml.add_constructor(u('!bunch'), from_yaml) yaml.add_constructor(u('!bunch.Bunch'), from_yaml) SafeRepresenter.add_representer(Bunch, to_yaml_safe) SafeRepresenter.add_multi_representer(Bunch, to_yaml_safe) Representer.add_representer(Bunch, to_yaml) Representer.add_multi_representer(Bunch, to_yaml) # Instance methods for YAML conversion def toYAML(self, **options): """ Serializes this Bunch to YAML, using ``yaml.safe_dump()`` if no ``Dumper`` is provided. See the PyYAML documentation for more info. >>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42) >>> import yaml >>> yaml.safe_dump(b, default_flow_style=True) '{foo: [bar, {lol: true}], hello: 42}\\n' >>> b.toYAML(default_flow_style=True) '{foo: [bar, {lol: true}], hello: 42}\\n' >>> yaml.dump(b, default_flow_style=True) '!bunch.Bunch {foo: [bar, !bunch.Bunch {lol: true}], hello: 42}\\n' >>> b.toYAML(Dumper=yaml.Dumper, default_flow_style=True) '!bunch.Bunch {foo: [bar, !bunch.Bunch {lol: true}], hello: 42}\\n' """ opts = dict(indent=2, default_flow_style=None) opts.update(options) if 'Dumper' not in opts: return yaml.safe_dump(self, **opts) else: return yaml.dump(self, **opts) def fromYAML(cls, *args, **kwargs): """ Convenience method for loading YAML and getting Bunches. >>> document = ''' ... foo: ... - bar ... - lol: true ... hello: 42 ... ''' >>> Bunch.fromYAML(document) Bunch(foo=['bar', Bunch(lol=True)], hello=42) Uses ``yaml.load()`` by default, but accepts the following for convenience: - ``safe=True`` for SafeLoader - ``full=True`` for FullLoader (default) - ``unsafe=True`` for UnsafeLoader - ``all=True`` to load all documents (returning a list) >>> documents = ''' ... --- ... - name: Hero ... level: 4 ... hp: 34 ... - name: Goblin ... level: 1 ... hp: 8 ... --- ... - name: Orc ... level: 2 ... hp: 12 ... ''' >>> Bunch.fromYAML(documents, all=True) #doctest: +NORMALIZE_WHITESPACE [[Bunch(hp=34, level=4, name='Hero'), Bunch(hp=8, level=1, name='Goblin')], [Bunch(hp=12, level=2, name='Orc')]] All other options are passed to PyYAML, so you can still specify a custom loader with ``Bunch.fromYAML(data, Loader=CustomLoader)``. (Note that supplying a kw argument ``Loader`` overrides ``safe``, ``full``, and ``unsafe``.) See https://msg.pyyaml.org/load for more info. """ method_name = 'full_load' for prefix in ('safe', 'full', 'unsafe'): # we want to pop all the prefix keys anyway, so put the test for Loader last if kwargs.pop(prefix, False) and 'Loader' not in kwargs: method_name = prefix+'_load' if kwargs.pop('all', False): data = list(getattr(yaml, method_name+'_all')(*args, **kwargs)) else: data = getattr(yaml, method_name)(*args, **kwargs) return bunchify(data, cls) Bunch.toYAML = toYAML Bunch.fromYAML = classmethod(fromYAML) except ImportError: pass if __name__ == "__main__": import doctest doctest.testmod()