【问题标题】:Python ruamel.yaml dumps tags with quotesPython ruamel.yaml 转储带有引号的标签
【发布时间】:2018-12-02 09:51:54
【问题描述】:

我正在尝试使用 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

我尝试过的其他事情:

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

【问题讨论】:

    标签: python yaml pyyaml ruamel.yaml


    【解决方案1】:

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

    仍然使用构造函数 sn-p 来加载 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 对象的键。问题很容易解决。

    【讨论】:

      【解决方案2】:

      基本上你调整加载器,加载标记(标量)对象,就好像它们是映射一样,标签是键,值是标量。但是您没有做任何事情来区分从此类映射加载的dict 与从正常映射加载的其他字典,也没有任何特定代码来表示这种映射以“取回标签”。

      当您尝试“创建”带有标签的标量时,您只需创建一个以感叹号开头的字符串,并且需要将其转储引用以将其与 真实 标记节点区分开来。

      混淆这一切的是,您的示例通过分配给base["foo"] 来覆盖加载的数据,因此您唯一可以从safe_load 以及您之前的所有代码派生的东西就是它不会抛出一个例外。 IE。如果您省略以 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,表明这是一个(输入)文件指针)。

      您的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
      

      这正是你想要的输出。

      【讨论】:

        猜你喜欢
        • 2020-05-30
        • 2022-01-17
        • 2019-01-16
        • 2014-10-26
        • 2021-05-13
        • 2023-03-21
        • 1970-01-01
        • 2017-06-24
        • 1970-01-01
        相关资源
        最近更新 更多