簡體   English   中英

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

我試過的其他事情:

  • pyyaml庫,工作原理相同
  • 使用Ref ::而不是!Ref,也一樣

本質上,你調整加載器,加載標記(標量)對象,就像它們是映射一樣,標記為鍵,值為標量。 但是你沒有做任何事情來區分從這樣的映射加載的dict和從正常映射加載的其他dicts,也沒有任何特定的代碼來表示這樣的映射“獲取標記”。

當您嘗試使用標記“創建”標量時,您只需創建一個以感嘆號開頭的字符串,並且需要轉儲引號以將其與實際標記的節點區分開來。

什么模糊這一切,是你的例子通過分配base["foo"]來覆蓋加載的數據所以你可以從safe_load和你之前的所有代碼中得到的唯一的東西是它不會拋出異常。 即如果你省略以base["foo"] = {開頭的行base["foo"] = {你的輸出將如下所示:

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

並且在那個Ref: bb與正常傾銷的dict無法區分。 如果你想探索這個路徑,那么你應該創建一個子類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 idem)

當作業更改為更接近您的作業時:

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對象的鍵。 問題很容易解決。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM