9

I have an YAML config that looks like:

config:
 - id: foo
 - name: bar
content:
 - run: xxx
 - remove: yyy

I am using Python YAML module to load it but I want to access it in better ways like:

stream = open(filename)
config = load(stream, Loader=Loader)
print(config['content'])

What I want is to be able to do: print(config.content).

sorin
  • 161,544
  • 178
  • 535
  • 806
  • 1
    Duplicate of http://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary – Justin Jun 15 '12 at 10:52
  • @Justin: This isn't a duplicate of this question, because you can simply patch the YAML loader to create objects of whatever class you desire rather than instances of `dict`. – Dietrich Epp Jun 15 '12 at 11:21

2 Answers2

13

You can use object notation with dictionaries using the following class, as discussed in this answer:

class DictAsMember(dict):
    def __getattr__(self, name):
        value = self[name]
        if isinstance(value, dict):
            value = DictAsMember(value)
        return value

This class in action:

>>> my_dict = DictAsMember(one=1, two=2)
>>> my_dict
{'two': 2, 'one': 1}
>>> my_dict.two
2

Edit This works recursively with sub-dictionaries, for example:

>>> my_dict = DictAsMember(one=1, two=2, subdict=dict(three=3, four=4))
>>> my_dict.one
1
>>> my_dict.subdict
{'four': 4, 'three': 3}
>>> my_dict.subdict.four
4
Community
  • 1
  • 1
Chris
  • 44,602
  • 16
  • 137
  • 156
  • I think that this does not work recursively, as you can imagine the same problem repeats for other entries in the config tree. – sorin Jun 15 '12 at 11:04
  • If I understand what you mean, this does work recursively. Does my edit answer your point? – Chris Jun 15 '12 at 11:14
  • 1
    Why is this (the simplest answer out there) not upvoted a million times? There are dozens of similar/duplicate questions that answer this in such a complicated way! – brandonscript Jan 19 '18 at 05:00
4

The easiest way to do this is probably to overwrite the YAML constructor for tag:yaml.org,2002:map so it returns a custom dictionary class instead of an ordinary dictionary.

import yaml

class AttrDict(object):
    def __init__(self, attr):
        self._attr = attr
    def __getattr__(self, attr):
        try:
            return self._attr[attr]
        except KeyError:
            raise AttributeError

def construct_map(self, node):
    # WARNING: This is copy/pasted without understanding!
    d = {}
    yield AttrDict(d)
    d.update(self.construct_mapping(node))

# WARNING: We are monkey patching PyYAML, and this will affect other clients!    
yaml.add_constructor('tag:yaml.org,2002:map', construct_map)

YAML = """
config:
  - id: foo
  - name: bar
content:
  - run: xxx
  - remove: yyy
"""

obj = yaml.load(YAML)

print(obj.config[0].id) # prints foo

Note that this will break everything else in the process that uses YAML, if it expects everything to work the normal Python way. You can use a custom loader, but I personally find the PyYAML documentation a bit labyrinthine, and it seems that side effects are global and contagious as a rule rather than an exception.

You have been warned.

As an alternative, if your schema is relatively static you could write your own classes and deserialize to those (e.g., class Config with id and name properties). It probably wouldn't be worth the cost of the extra code, however.

Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415