""" Munch is a subclass of dict with attribute-style access. >>> b = Munch() >>> b.hello = 'world' >>> b.hello 'world' >>> b['hello'] += "!" >>> b.hello 'world!' >>> b.foo = Munch(lol=True) >>> b.foo.lol True >>> b.foo is b['foo'] True It is safe to import * from this module: __all__ = ('Munch', 'munchify','unmunchify') un/munchify provide dictionary conversion; Munches can also be converted via Munch.to/fromDict(). """ from collections.abc import Mapping try: # For python 3.8 and later import importlib.metadata as importlib_metadata except ImportError: # For everyone else import importlib_metadata try: __version__ = importlib_metadata.version(__name__) except importlib_metadata.PackageNotFoundError: # package is not installed __version__ = "0.0.0" try: VERSION = tuple(map(int, __version__.split('+')[0].split('.')[:3])) except ValueError: VERSION = (0, 0, 0) __all__ = ('Munch', 'munchify', 'DefaultMunch', 'DefaultFactoryMunch', 'RecursiveMunch', 'unmunchify') class Munch(dict): """ A dictionary that provides attribute-style access. >>> b = Munch() >>> b.hello = 'world' >>> b.hello 'world' >>> b['hello'] += "!" >>> b.hello 'world!' >>> b.foo = Munch(lol=True) >>> b.foo.lol True >>> b.foo is b['foo'] True A Munch 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)) Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42}) As well as iteration... >>> sorted([ (k,b[k]) for k in b ]) [('foo', Munch({'lol': True})), ('hello', 42), ('ponies', 'are pretty!')] And "splats". >>> "The {knights} who say {ni}!".format(**Munch(knights='lolcats', ni='can haz')) 'The lolcats who say can haz!' See unmunchify/Munch.toDict, munchify/Munch.fromDict for notes about conversion. """ def __init__(self, *args, **kwargs): # pylint: disable=super-init-not-called self.update(*args, **kwargs) # 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 = Munch(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 Munch) will propagate as an AttributeError instead. >>> b = Munch(foo='bar', this_is='useful when subclassing') >>> hasattr(b.values, '__call__') True >>> 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: 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 = Munch(lol=42) >>> 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 toDict(self): """ Recursively converts a munch back into a dictionary. >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') >>> sorted(b.toDict().items()) [('foo', {'lol': True}), ('hello', 42), ('ponies', 'are pretty!')] See unmunchify for more info. """ return unmunchify(self) @property def __dict__(self): return self.toDict() def __repr__(self): """ Invertible* string-form of a Munch. >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') >>> print (repr(b)) Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42}) >>> eval(repr(b)) Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42}) >>> with_spaces = Munch({1: 2, 'a b': 9, 'c': Munch({'simple': 5})}) >>> print (repr(with_spaces)) Munch({'a b': 9, 1: 2, 'c': Munch({'simple': 5})}) >>> eval(repr(with_spaces)) Munch({'a b': 9, 1: 2, 'c': Munch({'simple': 5})}) (*) Invertible so long as collection contents are each repr-invertible. """ return f'{self.__class__.__name__}({dict.__repr__(self)})' def __dir__(self): return list(self.keys()) def __getstate__(self): """ Implement a serializable interface used for pickling. See https://docs.python.org/3.6/library/pickle.html. """ return {k: v for k, v in self.items()} def __setstate__(self, state): """ Implement a serializable interface used for pickling. See https://docs.python.org/3.6/library/pickle.html. """ self.clear() self.update(state) __members__ = __dir__ # for python2.x compatibility @classmethod def fromDict(cls, d): """ Recursively transforms a dictionary into a Munch via copy. >>> b = Munch.fromDict({'urmom': {'sez': {'what': 'what'}}}) >>> b.urmom.sez.what 'what' See munchify for more info. """ return munchify(d, cls) def copy(self): return type(self).fromDict(self) def update(self, *args, **kwargs): """ Override built-in method to call custom __setitem__ method that may be defined in subclasses. """ for k, v in dict(*args, **kwargs).items(): self[k] = v def get(self, k, d=None): """ D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None. """ if k not in self: return d return self[k] def setdefault(self, k, d=None): """ D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D """ if k not in self: self[k] = d return self[k] class AutoMunch(Munch): def __setattr__(self, k, v): """ Works the same as Munch.__setattr__ but if you supply a dictionary as value it will convert it to another Munch. """ if isinstance(v, Mapping) and not isinstance(v, (AutoMunch, Munch)): v = munchify(v, AutoMunch) super().__setattr__(k, v) class DefaultMunch(Munch): """ A Munch that returns a user-specified value for missing keys. """ def __init__(self, *args, **kwargs): """ Construct a new DefaultMunch. Like collections.defaultdict, the first argument is the default value; subsequent arguments are the same as those for dict. """ # Mimic collections.defaultdict constructor if args: default = args[0] args = args[1:] else: default = None super().__init__(*args, **kwargs) self.__default__ = default def __getattr__(self, k): """ Gets key if it exists, otherwise returns the default value.""" try: return super().__getattr__(k) except AttributeError: return self.__default__ def __setattr__(self, k, v): if k == '__default__': object.__setattr__(self, k, v) else: super().__setattr__(k, v) def __getitem__(self, k): """ Gets key if it exists, otherwise returns the default value.""" try: return super().__getitem__(k) except KeyError: return self.__default__ def __getstate__(self): """ Implement a serializable interface used for pickling. See https://docs.python.org/3.6/library/pickle.html. """ return (self.__default__, {k: v for k, v in self.items()}) def __setstate__(self, state): """ Implement a serializable interface used for pickling. See https://docs.python.org/3.6/library/pickle.html. """ self.clear() default, state_dict = state self.update(state_dict) self.__default__ = default @classmethod def fromDict(cls, d, default=None): # pylint: disable=arguments-differ return munchify(d, factory=lambda d_: cls(default, d_)) def copy(self): return type(self).fromDict(self, default=self.__default__) def __repr__(self): return f'{type(self).__name__}({self.__undefined__!r}, {dict.__repr__(self)})' class DefaultFactoryMunch(Munch): """ A Munch that calls a user-specified function to generate values for missing keys like collections.defaultdict. >>> b = DefaultFactoryMunch(list, {'hello': 'world!'}) >>> b.hello 'world!' >>> b.foo [] >>> b.bar.append('hello') >>> b.bar ['hello'] """ def __init__(self, default_factory, *args, **kwargs): super().__init__(*args, **kwargs) self.default_factory = default_factory @classmethod def fromDict(cls, d, default_factory): # pylint: disable=arguments-differ return munchify(d, factory=lambda d_: cls(default_factory, d_)) def copy(self): return type(self).fromDict(self, default_factory=self.default_factory) def __repr__(self): factory = self.default_factory.__name__ return f'{type(self).__name__}({factory}, {dict.__repr__(self)})' def __setattr__(self, k, v): if k == 'default_factory': object.__setattr__(self, k, v) else: super().__setattr__(k, v) def __missing__(self, k): self[k] = self.default_factory() return self[k] class RecursiveMunch(DefaultFactoryMunch): """A Munch that calls an instance of itself to generate values for missing keys. >>> b = RecursiveMunch({'hello': 'world!'}) >>> b.hello 'world!' >>> b.foo RecursiveMunch(RecursiveMunch, {}) >>> b.bar.okay = 'hello' >>> b.bar RecursiveMunch(RecursiveMunch, {'okay': 'hello'}) >>> b RecursiveMunch(RecursiveMunch, {'hello': 'world!', 'foo': RecursiveMunch(RecursiveMunch, {}), 'bar': RecursiveMunch(RecursiveMunch, {'okay': 'hello'})}) """ def __init__(self, *args, **kwargs): super().__init__(RecursiveMunch, *args, **kwargs) @classmethod def fromDict(cls, d): # pylint: disable=arguments-differ return munchify(d, factory=cls) def copy(self): return type(self).fromDict(self) # While we could convert abstract types like Mapping or Iterable, I think # munchify 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 munchify(x, factory=Munch): """ Recursively transforms a dictionary into a Munch via copy. >>> b = munchify({'urmom': {'sez': {'what': 'what'}}}) >>> b.urmom.sez.what 'what' munchify can handle intermediary dicts, lists and tuples (as well as their subclasses), but ymmv on custom datatypes. >>> b = munchify({ '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. """ # Munchify x, using `seen` to track object cycles seen = dict() def munchify_cycles(obj): partial, already_seen = pre_munchify_cycles(obj) if already_seen: return partial return post_munchify(partial, obj) def pre_munchify_cycles(obj): # If we've already begun munchifying obj, just return the already-created munchified obj try: return seen[id(obj)], True except KeyError: pass # Otherwise, first partly munchify obj (but without descending into any lists or dicts) and save that seen[id(obj)] = partial = pre_munchify(obj) return partial, False def pre_munchify(obj): # Here we return a skeleton of munchified obj, which is enough to save for later (in case # we need to break cycles) but it needs to filled out in post_munchify if isinstance(obj, Mapping): return factory({}) elif isinstance(obj, list): return type(obj)() elif isinstance(obj, tuple): type_factory = getattr(obj, "_make", type(obj)) return type_factory(pre_munchify_cycles(item)[0] for item in obj) else: return obj def post_munchify(partial, obj): # Here we finish munchifying the parts of obj that were deferred by pre_munchify because they # might be involved in a cycle if isinstance(obj, Mapping): partial.update((k, munchify_cycles(obj[k])) for k in obj.keys()) elif isinstance(obj, list): partial.extend(munchify_cycles(item) for item in obj) elif isinstance(obj, tuple): for (item_partial, item) in zip(partial, obj): post_munchify(item_partial, item) return partial return munchify_cycles(x) def unmunchify(x): """ Recursively converts a Munch into a dictionary. >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') >>> sorted(unmunchify(b).items()) [('foo', {'lol': True}), ('hello', 42), ('ponies', 'are pretty!')] unmunchify will handle intermediary dicts, lists and tuples (as well as their subclasses), but ymmv on custom datatypes. >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42, ... ponies=('are pretty!', Munch(lies='are trouble!'))) >>> sorted(unmunchify(b).items()) #doctest: +NORMALIZE_WHITESPACE [('foo', ['bar', {'lol': True}]), ('hello', 42), ('ponies', ('are pretty!', {'lies': 'are trouble!'}))] nb. As dicts are not hashable, they cannot be nested in sets/frozensets. """ # Munchify x, using `seen` to track object cycles seen = dict() def unmunchify_cycles(obj): # If we've already begun unmunchifying obj, just return the already-created unmunchified obj try: return seen[id(obj)] except KeyError: pass # Otherwise, first partly unmunchify obj (but without descending into any lists or dicts) and save that seen[id(obj)] = partial = pre_unmunchify(obj) # Then finish unmunchifying lists and dicts inside obj (reusing unmunchified obj if cycles are encountered) return post_unmunchify(partial, obj) def pre_unmunchify(obj): # Here we return a skeleton of unmunchified obj, which is enough to save for later (in case # we need to break cycles) but it needs to filled out in post_unmunchify if isinstance(obj, Mapping): return dict() elif isinstance(obj, list): return type(obj)() elif isinstance(obj, tuple): type_factory = getattr(obj, "_make", type(obj)) return type_factory(unmunchify_cycles(item) for item in obj) else: return obj def post_unmunchify(partial, obj): # Here we finish unmunchifying the parts of obj that were deferred by pre_unmunchify because they # might be involved in a cycle if isinstance(obj, Mapping): partial.update((k, unmunchify_cycles(obj[k])) for k in obj.keys()) elif isinstance(obj, list): partial.extend(unmunchify_cycles(v) for v in obj) elif isinstance(obj, tuple): for (value_partial, value) in zip(partial, obj): post_unmunchify(value_partial, value) return partial return unmunchify_cycles(x) # Serialization try: try: import json except ImportError: import simplejson as json def toJSON(self, **options): """ Serializes this Munch to JSON. Accepts the same keyword options as `json.dumps()`. >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') >>> json.dumps(b) == b.toJSON() True """ return json.dumps(self, **options) def fromJSON(cls, stream, *args, **kwargs): """ Deserializes JSON to Munch or any of its subclasses. """ factory = lambda d: cls(*(args + (d,)), **kwargs) return munchify(json.loads(stream), factory=factory) Munch.toJSON = toJSON Munch.fromJSON = classmethod(fromJSON) 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 Munches using the tag `!munch` and `!munch.Munch`. >>> import yaml >>> yaml.load(''' ... Flow style: !munch.Munch { Clark: Evans, Brian: Ingerson, Oren: Ben-Kiki } ... Block style: !munch ... Clark : Evans ... Brian : Ingerson ... Oren : Ben-Kiki ... ''') #doctest: +NORMALIZE_WHITESPACE {'Flow style': Munch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki'), 'Block style': Munch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki')} This module registers itself automatically to cover both Munch and any subclasses. Should you want to customize the representation of a subclass, simply register it with PyYAML yourself. """ data = Munch() yield data value = loader.construct_mapping(node) data.update(value) def to_yaml_safe(dumper, data): """ Converts Munch to a normal mapping node, making it appear as a dict in the YAML output. >>> b = Munch(foo=['bar', Munch(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 Munch to a representation node. >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42) >>> import yaml >>> yaml.dump(b, default_flow_style=True) '!munch.Munch {foo: [bar, !munch.Munch {lol: true}], hello: 42}\\n' """ return dumper.represent_mapping('!munch.Munch', data) for loader_name in ("BaseLoader", "FullLoader", "SafeLoader", "Loader", "UnsafeLoader", "DangerLoader"): LoaderCls = getattr(yaml, loader_name, None) if LoaderCls is None: # This code supports both PyYAML 4.x and 5.x versions continue yaml.add_constructor('!munch', from_yaml, Loader=LoaderCls) yaml.add_constructor('!munch.Munch', from_yaml, Loader=LoaderCls) SafeRepresenter.add_representer(Munch, to_yaml_safe) SafeRepresenter.add_multi_representer(Munch, to_yaml_safe) Representer.add_representer(Munch, to_yaml) Representer.add_multi_representer(Munch, to_yaml) # Instance methods for YAML conversion def toYAML(self, **options): """ Serializes this Munch to YAML, using `yaml.safe_dump()` if no `Dumper` is provided. See the PyYAML documentation for more info. >>> b = Munch(foo=['bar', Munch(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) '!munch.Munch {foo: [bar, !munch.Munch {lol: true}], hello: 42}\\n' >>> b.toYAML(Dumper=yaml.Dumper, default_flow_style=True) '!munch.Munch {foo: [bar, !munch.Munch {lol: true}], hello: 42}\\n' """ opts = dict(indent=4, default_flow_style=False) opts.update(options) if 'Dumper' not in opts: return yaml.safe_dump(self, **opts) else: return yaml.dump(self, **opts) def fromYAML(cls, stream, *args, **kwargs): factory = lambda d: cls(*(args + (d,)), **kwargs) loader_class = kwargs.pop('Loader', yaml.FullLoader) return munchify(yaml.load(stream, Loader=loader_class), factory=factory) Munch.toYAML = toYAML Munch.fromYAML = classmethod(fromYAML) except ImportError: pass