【问题标题】:Implementing "include" functionality via PyYAML & custom tags通过 PyYAML 和自定义标签实现“包含”功能
【发布时间】:2019-03-08 21:01:06
【问题描述】:

我正在尝试实现如下 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 中生成了顶级标签并且它可以工作(怪物)但是当我尝试走那条路线时我的代码失败了:

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)))

【问题讨论】:

    标签: yaml pyyaml


    【解决方案1】:

    标记在 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 所做的。

    您甚至可以完全不使用标记来执行上述操作,方法是为 +&lt; 添加解析器(以合并功能为例),子类化 SafeLoader 并实现 flatten_mapping 方法支持这种包容。然而,这意味着它并不那么明显 其他人认为有标签时会发生一些特别的事情。

    请注意:

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

    在 ruamel.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)
    

    给出:

    {'foo': 'bar', 'baz': 'buff', '+<': 'other/file.yaml', 'special': 'value'}
    

    在 PyYAML 中,您应该能够使用更多代码做类似的事情 并且仅支持 YAML 1.1 规范(该规范于 2009 年被取代)。

    【讨论】:

    • 您介意分享一些(伪)代码来说明吗?我尝试使用现有的 python 代码并未成功应用 YAML 更改(似乎将自定义标签 !Include 作为文档中的第一个条目与将其放在文档正文中的某个位置不同。
    • 我更新了我的答案。当然,我不能使用建议的 &gt;&gt; 键,因为 &gt; 开始折叠式文字标量 %-)
    猜你喜欢
    • 2021-09-28
    • 2012-01-14
    • 2017-08-20
    • 2011-01-23
    • 2012-12-22
    • 2020-02-14
    • 1970-01-01
    • 2012-12-11
    • 1970-01-01
    相关资源
    最近更新 更多