yaml use key or parent key as value

2019-08-03 07:31发布

I’ve just started using YAML (through pyyaml) and I was wondering if there is any way to state that the value of a key is the key name itself or the parent key. For example

---
foo: &FOO
  bar: !.
  baz: !..

foo2:
  <<: *FOO
…

{‘foo’: {‘bar’: ‘bar’, ‘baz’: ’foo’}, ‘foo2’:{‘bar’:’bar’, ‘baz’:’foo2’}}

(notice the dot and double dot on bar and baz respectively - those are just placeholders for getting the key name and parent key name)

I've tried using add_constructor:

def key_construct(loader, node):
    # return the key here
    pass

yaml.add_constructor('!.', key_construct)

but Node, doesn't hold the key (or a reference to the parent) and I couldn't find the way to get them.

EDIT:

So, here is my real use case and a solution based on Anthon's response: I have a logger configuration file (in yaml), and I wanted to reuse some definitions there:

handlers:
    base: &base_handler
        (): globals.TimedRotatingFileHandlerFactory
        name: ../
        when: midnight
        backupCount: 14

        level: DEBUG
        formatter: generic

    syslog:
        class: logging.handlers.SysLogHandler
        address: ['localhost', 514]
        facility: local5

        level: NOTSET
        formatter: syslog

    access:
        <<: *base_handler

    error:
        <<: *base_handler

loggers:
    base: &base_logger
        handlers: [../, syslog]
        qualname: ../

    access:
        <<: *base_logger

    error:
        <<: *base_logger
        handlers: [../, syslog, email]

The solution, as Anthon suggested was to traverse the configuration dictionary after is was being processed:

def expand_yaml(val, parent=None, key=None, key1=None, key2=None):
    if isinstance(val, str):
        if val == './':
            parent[key] = key1
        elif val == '../':
            parent[key] = key2
    elif isinstance(val, dict):
        for k, v in val.items():
            expand_yaml(v, val, k, k, key1)
    elif isinstance(val, list):
        parent[key] = val[:] # support inheritance of the list (as in *base_logger)
        for index, e in enumerate(parent[key]):
            expand_yaml(e, parent[key], index, key1, key2)
    return val

标签: yaml pyyaml
1条回答
乱世女痞
2楼-- · 2019-08-03 08:11

You don't have much context when you are constructing an element, so you are not going to find your key, and certainly not the parent key, to fill in the values, without digging in the call stack for the context (the loader knows about foo, bar and baz, but not in a way you can use to determine which is the corresponding key or parent_key).

What I suggest you do is create a special node that you return with the key_construct and then after the YAML load, walk the structure that your yaml.load() returned. Unless you have other ! objects, which make it more difficult to walk the resulting combination than a pure combination of sequences/lists and mappings/dicts ¹:

import ruamel.yaml as yaml

yaml_str = """\
foo: &FOO
  bar: !.
  baz: !..

foo2:
  <<: *FOO
"""

class Expander:
    def __init__(self, tag):
        self.tag = tag

    def expand(self, key, parent_key):
        if self.tag == '!.':
            return key
        elif self.tag == '!..':
            return parent_key
        raise NotImplementedError

    def __repr__(self):
        return "E({})".format(self.tag)

def expand(d, key=None, parent_key=None):
    if isinstance(d, list):
        for elem in d:
            expand(elem, key=key, parent_key=parent_key)
    elif isinstance(d, dict):
        for k in d:
            v = d[k]
            if isinstance(v, Expander):
                d[k] = v.expand(k, parent_key)
            expand(d[k], key, parent_key=k)
    return d


def key_construct(loader, node):
    return Expander(node.tag)

yaml.add_constructor('!.', key_construct)
yaml.add_constructor('!..', key_construct)


data = yaml.load(yaml_str) 
print(data)
print(expand(data))

gives you:

{'foo': {'bar': E(!.), 'baz': E(!..)}, 'foo2': {'bar': E(!.), 'baz': E(!..)}}
{'foo': {'bar': 'bar', 'baz': 'foo'}, 'foo2': {'bar': 'bar', 'baz': 'foo2'}}

¹ This was done using ruamel.yaml of which I am the author. PyYAML, of which ruamel.yaml is a functional superset, should work the same.

查看更多
登录 后发表回答