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