1""" Munch is a subclass of dict with attribute-style access.
2
3    >>> b = Munch()
4    >>> b.hello = 'world'
5    >>> b.hello
6    'world'
7    >>> b['hello'] += "!"
8    >>> b.hello
9    'world!'
10    >>> b.foo = Munch(lol=True)
11    >>> b.foo.lol
12    True
13    >>> b.foo is b['foo']
14    True
15
16    It is safe to import * from this module:
17
18        __all__ = ('Munch', 'munchify','unmunchify')
19
20    un/munchify provide dictionary conversion; Munches can also be
21    converted via Munch.to/fromDict().
22"""
23
24import pkg_resources
25
26from .python3_compat import iterkeys, iteritems, Mapping, u
27
28__version__ = pkg_resources.get_distribution('munch').version
29VERSION = tuple(map(int, __version__.split('.')[:3]))
30
31__all__ = ('Munch', 'munchify', 'DefaultMunch', 'DefaultFactoryMunch', 'unmunchify')
32
33
34
35class Munch(dict):
36    """ A dictionary that provides attribute-style access.
37
38        >>> b = Munch()
39        >>> b.hello = 'world'
40        >>> b.hello
41        'world'
42        >>> b['hello'] += "!"
43        >>> b.hello
44        'world!'
45        >>> b.foo = Munch(lol=True)
46        >>> b.foo.lol
47        True
48        >>> b.foo is b['foo']
49        True
50
51        A Munch is a subclass of dict; it supports all the methods a dict does...
52
53        >>> sorted(b.keys())
54        ['foo', 'hello']
55
56        Including update()...
57
58        >>> b.update({ 'ponies': 'are pretty!' }, hello=42)
59        >>> print (repr(b))
60        Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42})
61
62        As well as iteration...
63
64        >>> sorted([ (k,b[k]) for k in b ])
65        [('foo', Munch({'lol': True})), ('hello', 42), ('ponies', 'are pretty!')]
66
67        And "splats".
68
69        >>> "The {knights} who say {ni}!".format(**Munch(knights='lolcats', ni='can haz'))
70        'The lolcats who say can haz!'
71
72        See unmunchify/Munch.toDict, munchify/Munch.fromDict for notes about conversion.
73    """
74    def __init__(self, *args, **kwargs):  # pylint: disable=super-init-not-called
75        self.update(*args, **kwargs)
76
77    # only called if k not found in normal places
78    def __getattr__(self, k):
79        """ Gets key if it exists, otherwise throws AttributeError.
80
81            nb. __getattr__ is only called if key is not found in normal places.
82
83            >>> b = Munch(bar='baz', lol={})
84            >>> b.foo
85            Traceback (most recent call last):
86                ...
87            AttributeError: foo
88
89            >>> b.bar
90            'baz'
91            >>> getattr(b, 'bar')
92            'baz'
93            >>> b['bar']
94            'baz'
95
96            >>> b.lol is b['lol']
97            True
98            >>> b.lol is getattr(b, 'lol')
99            True
100        """
101        try:
102            # Throws exception if not in prototype chain
103            return object.__getattribute__(self, k)
104        except AttributeError:
105            try:
106                return self[k]
107            except KeyError:
108                raise AttributeError(k)
109
110    def __setattr__(self, k, v):
111        """ Sets attribute k if it exists, otherwise sets key k. A KeyError
112            raised by set-item (only likely if you subclass Munch) will
113            propagate as an AttributeError instead.
114
115            >>> b = Munch(foo='bar', this_is='useful when subclassing')
116            >>> hasattr(b.values, '__call__')
117            True
118            >>> b.values = 'uh oh'
119            >>> b.values
120            'uh oh'
121            >>> b['values']
122            Traceback (most recent call last):
123                ...
124            KeyError: 'values'
125        """
126        try:
127            # Throws exception if not in prototype chain
128            object.__getattribute__(self, k)
129        except AttributeError:
130            try:
131                self[k] = v
132            except:
133                raise AttributeError(k)
134        else:
135            object.__setattr__(self, k, v)
136
137    def __delattr__(self, k):
138        """ Deletes attribute k if it exists, otherwise deletes key k. A KeyError
139            raised by deleting the key--such as when the key is missing--will
140            propagate as an AttributeError instead.
141
142            >>> b = Munch(lol=42)
143            >>> del b.lol
144            >>> b.lol
145            Traceback (most recent call last):
146                ...
147            AttributeError: lol
148        """
149        try:
150            # Throws exception if not in prototype chain
151            object.__getattribute__(self, k)
152        except AttributeError:
153            try:
154                del self[k]
155            except KeyError:
156                raise AttributeError(k)
157        else:
158            object.__delattr__(self, k)
159
160    def toDict(self):
161        """ Recursively converts a munch back into a dictionary.
162
163            >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!')
164            >>> sorted(b.toDict().items())
165            [('foo', {'lol': True}), ('hello', 42), ('ponies', 'are pretty!')]
166
167            See unmunchify for more info.
168        """
169        return unmunchify(self)
170
171    @property
172    def __dict__(self):
173        return self.toDict()
174
175    def __repr__(self):
176        """ Invertible* string-form of a Munch.
177
178            >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!')
179            >>> print (repr(b))
180            Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42})
181            >>> eval(repr(b))
182            Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42})
183
184            >>> with_spaces = Munch({1: 2, 'a b': 9, 'c': Munch({'simple': 5})})
185            >>> print (repr(with_spaces))
186            Munch({'a b': 9, 1: 2, 'c': Munch({'simple': 5})})
187            >>> eval(repr(with_spaces))
188            Munch({'a b': 9, 1: 2, 'c': Munch({'simple': 5})})
189
190            (*) Invertible so long as collection contents are each repr-invertible.
191        """
192        return '{0}({1})'.format(self.__class__.__name__, dict.__repr__(self))
193
194    def __dir__(self):
195        return list(iterkeys(self))
196
197    def __getstate__(self):
198        """ Implement a serializable interface used for pickling.
199
200        See https://docs.python.org/3.6/library/pickle.html.
201        """
202        return {k: v for k, v in self.items()}
203
204    def __setstate__(self, state):
205        """ Implement a serializable interface used for pickling.
206
207        See https://docs.python.org/3.6/library/pickle.html.
208        """
209        self.clear()
210        self.update(state)
211
212    __members__ = __dir__  # for python2.x compatibility
213
214    @classmethod
215    def fromDict(cls, d):
216        """ Recursively transforms a dictionary into a Munch via copy.
217
218            >>> b = Munch.fromDict({'urmom': {'sez': {'what': 'what'}}})
219            >>> b.urmom.sez.what
220            'what'
221
222            See munchify for more info.
223        """
224        return munchify(d, cls)
225
226    def copy(self):
227        return type(self).fromDict(self)
228
229    def update(self, *args, **kwargs):
230        """
231        Override built-in method to call custom __setitem__ method that may
232        be defined in subclasses.
233        """
234        for k, v in iteritems(dict(*args, **kwargs)):
235            self[k] = v
236
237    def get(self, k, d=None):
238        """
239        D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.
240        """
241        if k not in self:
242            return d
243        return self[k]
244
245    def setdefault(self, k, d=None):
246        """
247        D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D
248        """
249        if k not in self:
250            self[k] = d
251        return self[k]
252
253
254class AutoMunch(Munch):
255    def __setattr__(self, k, v):
256        """ Works the same as Munch.__setattr__ but if you supply
257            a dictionary as value it will convert it to another Munch.
258        """
259        if isinstance(v, Mapping) and not isinstance(v, (AutoMunch, Munch)):
260            v = munchify(v, AutoMunch)
261        super(AutoMunch, self).__setattr__(k, v)
262
263
264class DefaultMunch(Munch):
265    """
266    A Munch that returns a user-specified value for missing keys.
267    """
268
269    def __init__(self, *args, **kwargs):
270        """ Construct a new DefaultMunch. Like collections.defaultdict, the
271            first argument is the default value; subsequent arguments are the
272            same as those for dict.
273        """
274        # Mimic collections.defaultdict constructor
275        if args:
276            default = args[0]
277            args = args[1:]
278        else:
279            default = None
280        super(DefaultMunch, self).__init__(*args, **kwargs)
281        self.__default__ = default
282
283    def __getattr__(self, k):
284        """ Gets key if it exists, otherwise returns the default value."""
285        try:
286            return super(DefaultMunch, self).__getattr__(k)
287        except AttributeError:
288            return self.__default__
289
290    def __setattr__(self, k, v):
291        if k == '__default__':
292            object.__setattr__(self, k, v)
293        else:
294            super(DefaultMunch, self).__setattr__(k, v)
295
296    def __getitem__(self, k):
297        """ Gets key if it exists, otherwise returns the default value."""
298        try:
299            return super(DefaultMunch, self).__getitem__(k)
300        except KeyError:
301            return self.__default__
302
303    def __getstate__(self):
304        """ Implement a serializable interface used for pickling.
305
306        See https://docs.python.org/3.6/library/pickle.html.
307        """
308        return (self.__default__, {k: v for k, v in self.items()})
309
310    def __setstate__(self, state):
311        """ Implement a serializable interface used for pickling.
312
313        See https://docs.python.org/3.6/library/pickle.html.
314        """
315        self.clear()
316        default, state_dict = state
317        self.update(state_dict)
318        self.__default__ = default
319
320    @classmethod
321    def fromDict(cls, d, default=None):
322        # pylint: disable=arguments-differ
323        return munchify(d, factory=lambda d_: cls(default, d_))
324
325    def copy(self):
326        return type(self).fromDict(self, default=self.__default__)
327
328    def __repr__(self):
329        return '{0}({1!r}, {2})'.format(
330            type(self).__name__, self.__undefined__, dict.__repr__(self))
331
332
333class DefaultFactoryMunch(Munch):
334    """ A Munch that calls a user-specified function to generate values for
335        missing keys like collections.defaultdict.
336
337        >>> b = DefaultFactoryMunch(list, {'hello': 'world!'})
338        >>> b.hello
339        'world!'
340        >>> b.foo
341        []
342        >>> b.bar.append('hello')
343        >>> b.bar
344        ['hello']
345    """
346
347    def __init__(self, default_factory, *args, **kwargs):
348        super(DefaultFactoryMunch, self).__init__(*args, **kwargs)
349        self.default_factory = default_factory
350
351    @classmethod
352    def fromDict(cls, d, default_factory):
353        # pylint: disable=arguments-differ
354        return munchify(d, factory=lambda d_: cls(default_factory, d_))
355
356    def copy(self):
357        return type(self).fromDict(self, default_factory=self.default_factory)
358
359    def __repr__(self):
360        factory = self.default_factory.__name__
361        return '{0}({1}, {2})'.format(
362            type(self).__name__, factory, dict.__repr__(self))
363
364    def __setattr__(self, k, v):
365        if k == 'default_factory':
366            object.__setattr__(self, k, v)
367        else:
368            super(DefaultFactoryMunch, self).__setattr__(k, v)
369
370    def __missing__(self, k):
371        self[k] = self.default_factory()
372        return self[k]
373
374
375# While we could convert abstract types like Mapping or Iterable, I think
376# munchify is more likely to "do what you mean" if it is conservative about
377# casting (ex: isinstance(str,Iterable) == True ).
378#
379# Should you disagree, it is not difficult to duplicate this function with
380# more aggressive coercion to suit your own purposes.
381
382def munchify(x, factory=Munch):
383    """ Recursively transforms a dictionary into a Munch via copy.
384
385        >>> b = munchify({'urmom': {'sez': {'what': 'what'}}})
386        >>> b.urmom.sez.what
387        'what'
388
389        munchify can handle intermediary dicts, lists and tuples (as well as
390        their subclasses), but ymmv on custom datatypes.
391
392        >>> b = munchify({ 'lol': ('cats', {'hah':'i win again'}),
393        ...         'hello': [{'french':'salut', 'german':'hallo'}] })
394        >>> b.hello[0].french
395        'salut'
396        >>> b.lol[1].hah
397        'i win again'
398
399        nb. As dicts are not hashable, they cannot be nested in sets/frozensets.
400    """
401    # Munchify x, using `seen` to track object cycles
402    seen = dict()
403
404    def munchify_cycles(obj):
405        # If we've already begun munchifying obj, just return the already-created munchified obj
406        try:
407            return seen[id(obj)]
408        except KeyError:
409            pass
410
411        # Otherwise, first partly munchify obj (but without descending into any lists or dicts) and save that
412        seen[id(obj)] = partial = pre_munchify(obj)
413        # Then finish munchifying lists and dicts inside obj (reusing munchified obj if cycles are encountered)
414        return post_munchify(partial, obj)
415
416    def pre_munchify(obj):
417        # Here we return a skeleton of munchified obj, which is enough to save for later (in case
418        # we need to break cycles) but it needs to filled out in post_munchify
419        if isinstance(obj, Mapping):
420            return factory({})
421        elif isinstance(obj, list):
422            return type(obj)()
423        elif isinstance(obj, tuple):
424            type_factory = getattr(obj, "_make", type(obj))
425            return type_factory(munchify_cycles(item) for item in obj)
426        else:
427            return obj
428
429    def post_munchify(partial, obj):
430        # Here we finish munchifying the parts of obj that were deferred by pre_munchify because they
431        # might be involved in a cycle
432        if isinstance(obj, Mapping):
433            partial.update((k, munchify_cycles(obj[k])) for k in iterkeys(obj))
434        elif isinstance(obj, list):
435            partial.extend(munchify_cycles(item) for item in obj)
436        elif isinstance(obj, tuple):
437            for (item_partial, item) in zip(partial, obj):
438                post_munchify(item_partial, item)
439
440        return partial
441
442    return munchify_cycles(x)
443
444
445def unmunchify(x):
446    """ Recursively converts a Munch into a dictionary.
447
448        >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!')
449        >>> sorted(unmunchify(b).items())
450        [('foo', {'lol': True}), ('hello', 42), ('ponies', 'are pretty!')]
451
452        unmunchify will handle intermediary dicts, lists and tuples (as well as
453        their subclasses), but ymmv on custom datatypes.
454
455        >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42,
456        ...         ponies=('are pretty!', Munch(lies='are trouble!')))
457        >>> sorted(unmunchify(b).items()) #doctest: +NORMALIZE_WHITESPACE
458        [('foo', ['bar', {'lol': True}]), ('hello', 42), ('ponies', ('are pretty!', {'lies': 'are trouble!'}))]
459
460        nb. As dicts are not hashable, they cannot be nested in sets/frozensets.
461    """
462
463    # Munchify x, using `seen` to track object cycles
464    seen = dict()
465
466    def unmunchify_cycles(obj):
467        # If we've already begun unmunchifying obj, just return the already-created unmunchified obj
468        try:
469            return seen[id(obj)]
470        except KeyError:
471            pass
472
473        # Otherwise, first partly unmunchify obj (but without descending into any lists or dicts) and save that
474        seen[id(obj)] = partial = pre_unmunchify(obj)
475        # Then finish unmunchifying lists and dicts inside obj (reusing unmunchified obj if cycles are encountered)
476        return post_unmunchify(partial, obj)
477
478    def pre_unmunchify(obj):
479        # Here we return a skeleton of unmunchified obj, which is enough to save for later (in case
480        # we need to break cycles) but it needs to filled out in post_unmunchify
481        if isinstance(obj, Mapping):
482            return dict()
483        elif isinstance(obj, list):
484            return type(obj)()
485        elif isinstance(obj, tuple):
486            type_factory = getattr(obj, "_make", type(obj))
487            return type_factory(unmunchify_cycles(item) for item in obj)
488        else:
489            return obj
490
491    def post_unmunchify(partial, obj):
492        # Here we finish unmunchifying the parts of obj that were deferred by pre_unmunchify because they
493        # might be involved in a cycle
494        if isinstance(obj, Mapping):
495            partial.update((k, unmunchify_cycles(obj[k])) for k in iterkeys(obj))
496        elif isinstance(obj, list):
497            partial.extend(unmunchify_cycles(v) for v in obj)
498        elif isinstance(obj, tuple):
499            for (value_partial, value) in zip(partial, obj):
500                post_unmunchify(value_partial, value)
501
502        return partial
503
504    return unmunchify_cycles(x)
505
506
507# Serialization
508
509try:
510    try:
511        import json
512    except ImportError:
513        import simplejson as json
514
515    def toJSON(self, **options):
516        """ Serializes this Munch to JSON. Accepts the same keyword options as `json.dumps()`.
517
518            >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!')
519            >>> json.dumps(b) == b.toJSON()
520            True
521        """
522        return json.dumps(self, **options)
523
524    def fromJSON(cls, stream, *args, **kwargs):
525        """ Deserializes JSON to Munch or any of its subclasses.
526        """
527        factory = lambda d: cls(*(args + (d,)), **kwargs)
528        return munchify(json.loads(stream), factory=factory)
529
530    Munch.toJSON = toJSON
531    Munch.fromJSON = classmethod(fromJSON)
532
533except ImportError:
534    pass
535
536
537try:
538    # Attempt to register ourself with PyYAML as a representer
539    import yaml
540    from yaml.representer import Representer, SafeRepresenter
541
542    def from_yaml(loader, node):
543        """ PyYAML support for Munches using the tag `!munch` and `!munch.Munch`.
544
545            >>> import yaml
546            >>> yaml.load('''
547            ... Flow style: !munch.Munch { Clark: Evans, Brian: Ingerson, Oren: Ben-Kiki }
548            ... Block style: !munch
549            ...   Clark : Evans
550            ...   Brian : Ingerson
551            ...   Oren  : Ben-Kiki
552            ... ''') #doctest: +NORMALIZE_WHITESPACE
553            {'Flow style': Munch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki'),
554             'Block style': Munch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki')}
555
556            This module registers itself automatically to cover both Munch and any
557            subclasses. Should you want to customize the representation of a subclass,
558            simply register it with PyYAML yourself.
559        """
560        data = Munch()
561        yield data
562        value = loader.construct_mapping(node)
563        data.update(value)
564
565    def to_yaml_safe(dumper, data):
566        """ Converts Munch to a normal mapping node, making it appear as a
567            dict in the YAML output.
568
569            >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42)
570            >>> import yaml
571            >>> yaml.safe_dump(b, default_flow_style=True)
572            '{foo: [bar, {lol: true}], hello: 42}\\n'
573        """
574        return dumper.represent_dict(data)
575
576    def to_yaml(dumper, data):
577        """ Converts Munch to a representation node.
578
579            >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42)
580            >>> import yaml
581            >>> yaml.dump(b, default_flow_style=True)
582            '!munch.Munch {foo: [bar, !munch.Munch {lol: true}], hello: 42}\\n'
583        """
584        return dumper.represent_mapping(u('!munch.Munch'), data)
585
586    for loader_name in ("BaseLoader", "FullLoader", "SafeLoader", "Loader", "UnsafeLoader", "DangerLoader"):
587        LoaderCls = getattr(yaml, loader_name, None)
588        if LoaderCls is None:
589            # This code supports both PyYAML 4.x and 5.x versions
590            continue
591        yaml.add_constructor(u('!munch'), from_yaml, Loader=LoaderCls)
592        yaml.add_constructor(u('!munch.Munch'), from_yaml, Loader=LoaderCls)
593
594    SafeRepresenter.add_representer(Munch, to_yaml_safe)
595    SafeRepresenter.add_multi_representer(Munch, to_yaml_safe)
596
597    Representer.add_representer(Munch, to_yaml)
598    Representer.add_multi_representer(Munch, to_yaml)
599
600    # Instance methods for YAML conversion
601    def toYAML(self, **options):
602        """ Serializes this Munch to YAML, using `yaml.safe_dump()` if
603            no `Dumper` is provided. See the PyYAML documentation for more info.
604
605            >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42)
606            >>> import yaml
607            >>> yaml.safe_dump(b, default_flow_style=True)
608            '{foo: [bar, {lol: true}], hello: 42}\\n'
609            >>> b.toYAML(default_flow_style=True)
610            '{foo: [bar, {lol: true}], hello: 42}\\n'
611            >>> yaml.dump(b, default_flow_style=True)
612            '!munch.Munch {foo: [bar, !munch.Munch {lol: true}], hello: 42}\\n'
613            >>> b.toYAML(Dumper=yaml.Dumper, default_flow_style=True)
614            '!munch.Munch {foo: [bar, !munch.Munch {lol: true}], hello: 42}\\n'
615
616        """
617        opts = dict(indent=4, default_flow_style=False)
618        opts.update(options)
619        if 'Dumper' not in opts:
620            return yaml.safe_dump(self, **opts)
621        else:
622            return yaml.dump(self, **opts)
623
624    def fromYAML(cls, stream, *args, **kwargs):
625        factory = lambda d: cls(*(args + (d,)), **kwargs)
626        loader_class = kwargs.pop('Loader', yaml.FullLoader)
627        return munchify(yaml.load(stream, Loader=loader_class), factory=factory)
628
629    Munch.toYAML = toYAML
630    Munch.fromYAML = classmethod(fromYAML)
631
632except ImportError:
633    pass
634