python - 将一个 Yaml 文件包含在另一个文件中

标签 python yaml config pyyaml ruamel.yaml

我想要一个基础配置文件,其他配置文件使用它来共享通用配置。

例如,如果我有一个文件 base.yml with

foo: 1

bar:
  - 2
  - 3

然后是第二个文件 some_file.yml

foo: 2

baz: "baz"

我希望以合并的配置文件结束

foo: 2

bar:
  - 2
  - 3

baz: "baz"

编写处理 !include 标记的自定义加载程序非常容易。

class ConfigLoader(yaml.SafeLoader):

    def __init__(self, stream):
        super().__init__(stream)
        self._base = Path(stream.name).parent

    def include(self, node):
        file_name = self.construct_scalar(node)
        file_path = self._base.joinpath(file_name)

        with file_path.open("rt") as fh:
            return yaml.load(fh, IncludeLoader)

然后我可以解析 !include 标签。所以如果我的文件是

inherit:
   !include base.yml

foo: 2

baz: "baz"

但现在基本配置是一个映射。 IE。如果我加载文件,我将得到

config = {'a': [42], 'c': [3.6, [1, 2, 3]], 'include': [{'a': 1, 'b': [1.43, 543.55]}]}

但是如果我不让标签成为映射的一部分,例如

!include base.yml

foo: 2

baz: "baz"

我得到一个错误。 yaml.scanner.ScannerError:此处不允许映射值

但我知道 yaml 解析器可以在不需要映射的情况下解析标签。因为我可以做这样的事情

!!python/object:foo.Bar
x: 1.0   
y: 3.14

那么我该如何编写加载程序和/或构造我的 YAML 文件,以便我可以在我的配置中包含另一个文件?

最佳答案

在 YAML 中,您不能混合使用标量、映射键和序列元素。这是无效的 YAML:

- abc
d: e

这也是

some_file_name
a: b

并且你引用了那个标量,并提供了一个标签当然不会改变事实 它是无效的 YAML。

正如您已经发现的那样,您可以诱使加载程序返回一个 dict 而不是 字符串(就像解析器已经为非基本类型(如 datetime.date)内置构造函数一样)。

那个:

!!python/object:foo.Bar
x: 1.0
y: 3.14

之所以可行,是因为整个映射都被标记了,您只需标记一个标量值即可。

什么也是无效语法:

!include base.yaml
foo: 2
baz: baz

但你可以这样做:

!include
filename: base.yaml
foo: 2
baz: baz

并以特殊方式处理 'filename' 键,或者使 !include 标记一个空键:

!include : base.yaml  # : is a valid tag character, so you need the space
foo: 2
baz: baz

不过,我会考虑使用合并键,因为合并本质上就是 你正在尝试做。以下 YAML 有效:

import sys
import ruamel.yaml
from pathlib import Path

yaml_str = """
<<: {x: 42, y: 196, foo: 3}
foo: 2
baz: baz
"""
yaml = ruamel.yaml.YAML(typ='safe')
yaml.default_flow_style = False
data = yaml.load(yaml_str)
yaml.dump(data, sys.stdout)

给出:

baz: baz
foo: 2
x: 42
y: 196

所以你应该能够做到:

<<: !load base.yaml
foo: 2
baz: baz

任何了解合并键的人都知道如果 base.yaml 包含键 foo 和值 3 会发生什么, 也会明白:

<<: [!load base.yaml, !load config.yaml]
foo: 2
baz: baz

(因为我倾向于将“包含”与文本包含联系起来,就像在 C 预处理器中一样,我认为 `!load' 可能是一个 更合适的标签,但这可能是个人喜好问题)。

要使合并键起作用,最简单的方法可能是子类化 Constructor,因为合并是在标记解析之前完成的:

import sys
import ruamel.yaml
from ruamel.yaml.nodes import MappingNode, SequenceNode, ScalarNode
from ruamel.yaml.constructor import ConstructorError
from ruamel.yaml.compat import _F
from pathlib import Path



class MyConstructor(ruamel.yaml.constructor.SafeConstructor):
    def flatten_mapping(self, node):
        # type: (Any) -> Any
        """
        This implements the merge key feature http://yaml.org/type/merge.html
        by inserting keys from the merge dict/list of dicts if not yet
        available in this node
        """
        merge = []  # type: List[Any]
        index = 0
        while index < len(node.value):
            key_node, value_node = node.value[index]
            if key_node.tag == 'tag:yaml.org,2002:merge':
                if merge:  # double << key
                    if self.allow_duplicate_keys:
                        del node.value[index]
                        index += 1
                        continue
                    args = [
                        'while constructing a mapping',
                        node.start_mark,
                        'found duplicate key "{}"'.format(key_node.value),
                        key_node.start_mark,
                        """
                        To suppress this check see:
                           http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys
                        """,
                        """\
                        Duplicate keys will become an error in future releases, and are errors
                        by default when using the new API.
                        """,
                    ]
                    if self.allow_duplicate_keys is None:
                        warnings.warn(DuplicateKeyFutureWarning(*args))
                    else:
                        raise DuplicateKeyError(*args)
                del node.value[index]
                if isinstance(value_node, ScalarNode) and value_node.tag == '!load':
                    file_path = None
                    try:
                        if self.loader.reader.stream is not None:
                            file_path = Path(self.loader.reader.stream.name).parent / value_node.value
                    except AttributeError:
                        pass
                    if file_path is None:
                        file_path = Path(value_node.value)
                    # there is a bug in ruamel.yaml<=0.17.20 that prevents
                    # the use of a Path as argument to compose()
                    with file_path.open('rb') as fp:
                        merge.extend(ruamel.yaml.YAML().compose(fp).value)
                elif isinstance(value_node, MappingNode):
                    self.flatten_mapping(value_node)
                    print('vn0', type(value_node.value), value_node.value)
                    merge.extend(value_node.value)
                elif isinstance(value_node, SequenceNode):
                    submerge = []
                    for subnode in value_node.value:
                        if not isinstance(subnode, MappingNode):
                            raise ConstructorError(
                                'while constructing a mapping',
                                node.start_mark,
                                _F(
                                    'expected a mapping for merging, but found {subnode_id!s}',
                                    subnode_id=subnode.id,
                                ),
                                subnode.start_mark,
                            )
                        self.flatten_mapping(subnode)
                        submerge.append(subnode.value)
                    submerge.reverse()
                    for value in submerge:
                        merge.extend(value)
                else:
                    raise ConstructorError(
                        'while constructing a mapping',
                        node.start_mark,
                        _F(
                            'expected a mapping or list of mappings for merging, '
                            'but found {value_node_id!s}',
                            value_node_id=value_node.id,
                        ),
                        value_node.start_mark,
                    )
            elif key_node.tag == 'tag:yaml.org,2002:value':
                key_node.tag = 'tag:yaml.org,2002:str'
                index += 1
            else:
                index += 1
        if bool(merge):
            node.merge = merge  # separate merge keys to be able to update without duplicate
            node.value = merge + node.value


yaml = ruamel.yaml.YAML(typ='safe', pure=True)
yaml.default_flow_style = False
yaml.Constructor = MyConstructor



yaml_str = """\
<<: !load base.yaml
foo: 2
baz: baz
"""

data = yaml.load(yaml_str)
yaml.dump(data, sys.stdout)
print('---')

file_name = Path('test.yaml')
file_name.write_text("""\
<<: !load base.yaml
bar: 2
baz: baz
""")

data = yaml.load(file_name)
yaml.dump(data, sys.stdout)

这打印:

bar:
- 2
- 3
baz: baz
foo: 2
---
bar: 2
baz: baz
foo: 1

注意事项:

  • 不要以文本形式打开 YAML 文件。它们是二进制 (UTF-8) 格式的,您应该这样加载它们 (open(filename, 'rb'))。
  • 如果您在问题中包含了一个完整的工作程序(或者至少包含了 IncludeLoader 的文本,它 将有 可以用合并键提供一个完整的工作示例(或者为你找出它 由于某种原因不起作用)
  • 事实上,不清楚您的 yaml.load() 是否是实例方法调用(import ruamel.yaml; yaml = ruamel.yaml.YAML()) 或调用函数(from ruamel import yaml)。您不应使用后者,因为它已被弃用。

关于python - 将一个 Yaml 文件包含在另一个文件中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71075798/

相关文章:

python - NetfilterQueue set_payload 不起作用

c++ - 在 RAD Studio 2010 中编译 yaml-cpp 时出错

debugging - 如何找到 ".emacs"或 "init.el"中的错误?

python yaml.dump 其他YAML格式的格式列表

azure - 失败的测试不会使 YAML 中的任务失败

version-control - 与HG服务一起提供多个存储库。如何?

javascript - 不同文件中的Requirejs配置

python - pyparsing 删除一些文本以及如何使用空格捕获文本

python - os.path.exists 无法识别 C :\Windows\system32\drivers 下的子目录

python - 正确使用 numpy searchsorted 例程