通过PyYAML和自定义标签实现“包含”功能

时间:2018-10-03 15:12:55

标签: yaml pyyaml

我正在尝试实现如下所示的YAML语法:

---
foo: bar
baz: buff
!Include: other/file

,并在加载时由PyYAML处理,以使other/file.yml的内容:

---
special: value

与原始文件的内容合并,产生:

---
foo: bar
baz: buff
special: value

到目前为止,我已经遵循PyYAML DocsCreating Custom Tag in PyYAML并能够实现一些尴尬的实现:

---
foo: bar
baz: buff
included: !Include other/file

其翻译为:

---
foo: bar
baz: buff
included:
  special: value

我看到PyYAML Docs中正在生成顶级标签,并且该标签可以正常工作(Monster),但是当我尝试采用该路线时,我的代码失败了:

yaml.scanner.ScannerError: while scanning a simple key
  in "sample.yml", line 4, column 1
could not find expected ':'
  in "sample.yml", line 5, column 1

当前代码:

import yaml
import sys
from UserDict import UserDict


class Include(yaml.YAMLObject, UserDict):
    yaml_tag = u'!Include'

    def __init__(self, path):
        self.path = path
        data = {}
        try:
            with open(self.path+'.yml', 'r') as f:
                data = yaml.load(f)
        except IOError:
            data = {}
        self.data = data

    def __str__(self):
        return str(self.data)

    def __repr__(self):
        return "{0}(path={1})".format(self.__class__.__name__, self.path)

    @classmethod
    def from_yaml(cls, loader, node):
        return Include(node.value).data

    @classmethod
    def to_yaml(cls, dumper, data):
        return dumper.represent_scalar(cls.yaml_tag, data.path)

yaml.SafeLoader.add_constructor(u'!Include', Include.from_yaml)
yaml.add_constructor(u'!Include', Include.from_yaml)
# Required for safe_dump
yaml.SafeDumper.add_multi_representer(Include, Include.to_yaml)
yaml.add_multi_representer(Include, Include.to_yaml)

if __name__ == '__main__':
    fname = sys.argv[1]
    f = open(fname, 'r')
    data = yaml.safe_load(f)
    print("{0}".format(str(data)))

1 个答案:

答案 0 :(得分:0)

标记在YAML中的含义与在现实世界中相同:向某些对象提供标记。标签不能替代该对象。

当您执行以下操作时:!Include: other/file,等同于进行!Include Null: other/file。而且,在解析该Null节点时,您无权访问other/file,该节点不会被解析,甚至可能尚未被扫描。

在解析included: !Include other/file时,节点other/file并不具有其上下文的概念。它可以是例如堆栈的形式,您可以从中访问最新的对象,但是现在这就是实现PyYAML的方式。 这样做的含义是,如果执行此操作,则只能用从包含文件加载的数据结构替换标记的节点。

您可以做的是定义一个“特殊键”,例如+<,然后将!Include标签放在映射上:

!Include
foo: bar
baz: buff
+<: other/file

这样,您就可以实现映射的构造函数以创建字典,但是在遇到特殊键时,请使用与键相关联的值作为要加载的文件的名称,并将其作为其他键/值插入到您要查询的字典中构建(因此您不必访问一些不可用的上下文节点)。您将必须想出一些方法来解决来自包含文件中的键和来自实际包含映射的键的优先级。您可以通过允许该值成为文件名列表来实现多个包含。这与merge key language independent type相似。

通过为+<添加一个解析器(以用于合并功能的解析器为例),将SafeLoader子类化并实现{ {1}}方法来支持此包含。但这意味着,对于 其他人有特殊的事情,比如有标签的时候。

请注意:

  • 您应该省略指令末尾分隔符(flatten_mapping),因为您没有任何指令,所以它是多余的。
  • 您的YAML文件应具有扩展名--- unless that is not possible(例如,因为文件系统不支持超过三个字符的后缀)

在ruamel.yaml中,您可以使用以下方法实现此目标:

.yaml

给出:

import sys
import ruamel.yaml
from pathlib import Path

yaml = ruamel.yaml.YAML(typ='safe', pure=True)

yaml_str = """\
!Include
foo: bar
baz: buff
+<: other/file.yaml
"""

class Include:
    @classmethod
    def from_yaml(cls, constructor, node):
        mapping = constructor.construct_mapping(node)
        file_names = mapping.get('+<')
        if file_names is None:
            return mapping
        if not isinstance(file_names, list):
            file_names = [file_names]
        y = constructor.loader
        yaml = ruamel.yaml.YAML(typ=y.typ, pure=y.pure)
        for file_name in file_names:
            for key, value in yaml.load(Path(file_name)).items():
                if key in mapping:
                    continue
                mapping[key] = value
        return mapping


yaml.register_class(Include)

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

在PyYAML中,您应该能够执行类似的操作,并添加更多代码 并且仅支持YAML 1.1规范(该规范已于2009年被取代)。