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

时间:2018-06-22 21:57:30

标签: python yaml pyyaml ruamel.yaml

我正在尝试使用ruamel.yaml通过python即时修改AWS CloudFormation模板。我添加了以下代码,以使safe_load与诸如!Ref之类的CloudFormation函数一起使用。但是,当我转储它们时,带有!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

我尝试过的其他事情:

  • pyyaml库,工作原理相同
  • 使用Ref ::代替!Ref, 一样

2 个答案:

答案 0 :(得分:2)

基本上,您需要调整加载程序,以加载标记的(标量)对象,就像它们是映射一样,使用标记键和标量值。但是,您没有做任何区分从这种映射加载的dict与从普通映射加载的其他dict的区分,也没有任何特定的代码来表示这种映射以“找回标签”。

当您尝试使用标签“创建”标量时,您只需制作一个以感叹号开头的字符串,并且需要将其引号引起来以将其与 real 标记的节点区分开。 / p>

使这一切感到困惑的是,您的示例通过分配给base["foo"]来覆盖已加载的数据,因此您唯一可以从safe_load派生的内容以及之前的所有代码是不会抛出异常。即如果您省略以base["foo"] = {开头的行,则输出将如下所示:

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

而且Ref: bb与普通的转储字典没有区别。如果要探索这条路线,则应创建一个子类TagDict(dict),并让funcparse返回该子类,并为该子类添加一个representer,以便重新创建键中的标签,然后转储值。一旦可行(往返等于输入),您可以执行以下操作:

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

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

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

(我也将省略"r"(或在其前面放置一个空格;并用更合适的变量名称替换txt来指示这是一个(输入)文件指针)。 / p>

您在'Split'中也有两次输入funcnames,这是多余的。


使用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__的子类)并提供适当的{{ 1}}(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

这正是您想要的输出。

答案 1 :(得分:0)

除了上面的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对象的键。问题很容易解决。