Python ruamel.yaml 转储带引号的标签

Python ruamel.yaml dumps tags with quotes

我正在尝试使用 ruamel.yaml 通过 python 动态修改 AWS CloudFormation 模板。我添加了以下代码以使 safe_load 与 CloudFormation 函数(例如 !Ref)一起工作。但是,当我将它们转储出来时,那些带有 !Ref (或任何其他函数)的值将用引号引起来。 CloudFormation 无法识别。

参见下面的示例:

import sys, json, io, boto3
import ruamel.yaml

def funcparse(loader, node):
  node.value = {
      ruamel.yaml.ScalarNode:   loader.construct_scalar,
      ruamel.yaml.SequenceNode: loader.construct_sequence,
      ruamel.yaml.MappingNode:  loader.construct_mapping,
  }[type(node)](node)
  node.tag = node.tag.replace(u'!Ref', 'Ref').replace(u'!', u'Fn::')
  return dict([ (node.tag, node.value) ])

funcnames = [ 'Ref', 'Base64', 'FindInMap', 'GetAtt', 'GetAZs', 'ImportValue',
              'Join', 'Select', 'Split', 'Split', 'Sub', 'And', 'Equals', 'If',
              'Not', 'Or' ]

for func in funcnames:
    ruamel.yaml.SafeLoader.add_constructor(u'!' + func, funcparse)

txt = open("/space/tmp/a.template","r")
base = ruamel.yaml.safe_load(txt)
base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : "!Ref aaa",
        "VpcPeeringConnectionId" : "!Ref bbb",
        "yourname": "dfw"
    }
}

ruamel.yaml.safe_dump(
    base,
    sys.stdout,
    default_flow_style=False
)

输入文件是这样的:

foo:
  bar: !Ref barr
  aa: !Ref bb

输出是这样的:

foo:
  Resources:
    RouteTableId: '!Ref aaa'
    VpcPeeringConnectionId: '!Ref bbb'
    yourname: dfw
  name: abc

请注意“!Ref VpcRouteTable”用单引号引起来。这不会被 CloudFormation 识别。有没有办法配置转储程序,使输出类似于:

foo:
  Resources:
    RouteTableId: !Ref aaa
    VpcPeeringConnectionId: !Ref bbb
    yourname: dfw
  name: abc

我尝试过的其他事情:

本质上,您调整加载程序,加载标记(标量)对象,就好像它们是映射一样,标签是键,值是标量。但是你没有做任何事情来区分从这样的映射加载的 dict 和从正常映射加载的其他字典,你也没有任何特定的代码来表示到 "get the tag back" 的映射。

当您尝试 "create" 带有标签的标量时,您只是创建了一个以感叹号开头的字符串,并且需要转储引号以将其与 真正的 标记节点。

令人困惑的是,您的示例通过分配给 base["foo"] 来覆盖加载的数据,因此您唯一可以从 safe_load 派生的东西,以及之前的所有代码,是它不会抛出异常。 IE。如果您省略以 base["foo"] = { 开头的行,您的输出将如下所示:

foo:
  aa:
    Ref: bb
  bar:
    Ref: barr

并且 Ref: bb 与普通的转储字典没有区别。如果你想探索这条路线,那么你应该创建一个子类TagDict(dict),并且有funcparse return那个子类,并且还添加一个representer用于该子类根据键重新创建标签,然后转储值。一旦成功(往返等于输入),您可以:

     "RouteTableId" : TagDict('Ref', 'aaa')

如果这样做,除了删除未使用的库之外,还应该更改代码以关闭代码中的文件指针 txt,因为这可能会导致问题。您可以使用 with 语句优雅地做到这一点:

with open("/space/tmp/a.template","r") as txt:
    base = ruamel.yaml.safe_load(txt)

(我也会省略 "r"(或在它前面放一个 space);并用更合适的变量名替换 txt,表明这是一个(输入) 文件指针).

您的 funcnames 中还有两次条目 'Split',这是多余的。


更通用的解决方案可以通过使用 multi-constructor 匹配任何标签并具有三种基本类型来涵盖标量、映射和序列来实现。

import sys
import ruamel.yaml

yaml_str = """\
foo:
  scalar: !Ref barr
  mapping: !Select
    a: !Ref 1
    b: !Base64 A413
  sequence: !Split
  - !Ref baz
  - !Split Multi word scalar
"""

class Generic:
    def __init__(self, tag, value, style=None):
        self._value = value
        self._tag = tag
        self._style = style


class GenericScalar(Generic):
    @classmethod
    def to_yaml(self, representer, node):
        return representer.represent_scalar(node._tag, node._value)

    @staticmethod
    def construct(constructor, node):
        return constructor.construct_scalar(node)


class GenericMapping(Generic):
    @classmethod
    def to_yaml(self, representer, node):
        return representer.represent_mapping(node._tag, node._value)

    @staticmethod
    def construct(constructor, node):
        return constructor.construct_mapping(node, deep=True)


class GenericSequence(Generic):
    @classmethod
    def to_yaml(self, representer, node):
        return representer.represent_sequence(node._tag, node._value)

    @staticmethod
    def construct(constructor, node):
        return constructor.construct_sequence(node, deep=True)


def default_constructor(constructor, tag_suffix, node):
    generic = {
        ruamel.yaml.ScalarNode: GenericScalar,
        ruamel.yaml.MappingNode: GenericMapping,
        ruamel.yaml.SequenceNode: GenericSequence,
    }.get(type(node))
    if generic is None:
        raise NotImplementedError('Node: ' + str(type(node)))
    style = getattr(node, 'style', None)
    instance = generic.__new__(generic)
    yield instance
    state = generic.construct(constructor, node)
    instance.__init__(tag_suffix, state, style=style)


ruamel.yaml.add_multi_constructor('', default_constructor, Loader=ruamel.yaml.SafeLoader)


yaml = ruamel.yaml.YAML(typ='safe', pure=True)
yaml.default_flow_style = False
yaml.register_class(GenericScalar)
yaml.register_class(GenericMapping)
yaml.register_class(GenericSequence)

base = yaml.load(yaml_str)
base['bar'] = {
    'name': 'abc',
    'Resources': {
        'RouteTableId' : GenericScalar('!Ref', 'aaa'),
        'VpcPeeringConnectionId' : GenericScalar('!Ref', 'bbb'),
        'yourname': 'dfw',
        's' : GenericSequence('!Split', ['a', GenericScalar('!Not', 'b'), 'c']),
    }
}
yaml.dump(base, sys.stdout)

输出:

bar:
  Resources:
    RouteTableId: !Ref aaa
    VpcPeeringConnectionId: !Ref bbb
    s: !Split
    - a
    - !Not b
    - c
    yourname: dfw
  name: abc
foo:
  mapping: !Select
    a: !Ref 1
    b: !Base64 A413
  scalar: !Ref barr
  sequence: !Split
  - !Ref baz
  - !Split Multi word scalar

请注意序列和映射已正确处理,并且也可以创建它们。但是没有检查:

  • 您提供的标签确实有效
  • 与标签关联的值是该标签名称的正确类型(标量、映射、序列)
  • 如果您希望 GenericMapping 表现得更像 dict,那么您可能希望它成为 dict(而不是 Generic)的子类,并提供适当的__init__(同上 GenericSequence/list

当作业更改为更接近您的作业时:

base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : GenericScalar('!Ref', 'aaa'),
        "VpcPeeringConnectionId" : GenericScalar('!Ref', 'bbb'),
        "yourname": "dfw"
    }
}

输出为:

foo:
  Resources:
    RouteTableId: !Ref aaa
    VpcPeeringConnectionId: !Ref bbb
    yourname: dfw
  name: abc

这正是您想要的输出。

除了上面 Anthon 的详细回答之外,对于 CloudFormation 模板方面的具体问题,我找到了另一个非常快速和有效的解决方法。

仍在使用构造函数片段加载 YAML。

def funcparse(loader, node):
  node.value = {
      ruamel.yaml.ScalarNode:   loader.construct_scalar,
      ruamel.yaml.SequenceNode: loader.construct_sequence,
      ruamel.yaml.MappingNode:  loader.construct_mapping,
  }[type(node)](node)
  node.tag = node.tag.replace(u'!Ref', 'Ref').replace(u'!', u'Fn::')
  return dict([ (node.tag, node.value) ])

funcnames = [ 'Ref', 'Base64', 'FindInMap', 'GetAtt', 'GetAZs', 'ImportValue',
              'Join', 'Select', 'Split', 'Split', 'Sub', 'And', 'Equals', 'If',
              'Not', 'Or' ]

for func in funcnames:
    ruamel.yaml.SafeLoader.add_constructor(u'!' + func, funcparse)

当我们操作数据时,而不是

base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : "!Ref aaa",
        "VpcPeeringConnectionId" : "!Ref bbb",
        "yourname": "dfw"
    }
}

这会将值 !Ref aaa 用引号括起来,我们可以简单地做:

base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : {
            "Ref" : "aaa"
        },
        "VpcPeeringConnectionId" : {
            "Ref" : "bbb
         },
        "yourname": "dfw"
    }
}

类似地,对于 CloudFormation 中的其他函数,例如 !GetAtt,我们应该使用它们的长格式 Fn::GetAtt 并将它们用作 JSON 对象的键。问题轻松解决。