2017-05-07 88 views
1

最近我试图用ruamel.yaml来管理我的docker-compose服务配置(即docker-compose.yml)。如何使用ruamel.yaml注释掉YAML部分?

我需要注释掉&需要时取消注释服务块。假设我有以下文件:

version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    srv2: 
     image: alpine 
     container_name: srv2 
     volumes_from: 
      - some-volume 
volumes: 
    some-volume: 

有一些解决方法,以注释掉SRV2块?像下面的输出:

version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    #srv2: 
    # image: alpine 
    # container_name: srv2 
    # volumes_from: 
    #  - some-volume 
volumes: 
    some-volume: 

此外,有一种方法来去掉这一块?(假设我已经持有原来srv2块,我只需要一种方法来删除这些注释行)

回答

1

如果srv2是一个关键是为所有在你的YAML映射的唯一,那么“容易”的方式要循环使用直线,测试去除线条的版本是否以srv2:开头,请注意前导空格的数量并注释掉该行,并按照直到您注意到具有等于或少于前导空格的行。这样做的好处除了简单和快捷以外,它可以处理不规则的缩进(例如:在srv1之前的4个位置和在some-volume之前的6个位置)。

在使用ruamel.yaml时也可以这样做,但不太简单。您必须知道,当round_trip_loading时,ruamel.yaml通常会对已处理的最后一个结构(映射/序列)附加注释,并且由于该注释的结果srv1在您的示例中完全不同于srv2(即第一个键值对,如果注释掉,则不同于所有其他键值对)。

如果您正常化的预期输出到四个位置缩进和srv1之前添加注释供分析之用,加载,你可以搜索在评论结束:

from ruamel.yaml.util import load_yaml_guess_indent 

yaml_str = """\ 
version: '2' 
services: 
    #a 
    #b 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    #srv2: 
    # image: alpine 
    # container_name: srv2 
    # volumes_from: 
    #  - some-volume 
volumes: 
    some-volume: 
""" 

data, indent, block_seq_indent = load_yaml_guess_indent(yaml_str) 
print('indent', indent, block_seq_indent) 

c0 = data['services'].ca 
print('c0:', c0) 
c0_0 = c0.comment[1][0] 
print('c0_0:', repr(c0_0.value), c0_0.start_mark.column) 

c1 = data['services']['srv1']['volumes'].ca 
print('c1:', c1) 
c1_0 = c1.end[0] 
print('c1_0:', repr(c1_0.value), c1_0.start_mark.column) 

它打印:

indent 4 2 
c0: Comment(comment=[None, [CommentToken(), CommentToken()]], 
    items={}) 
c0_0: '#a\n' 4 
c1: Comment(comment=[None, None], 
    items={}, 
    end=[CommentToken(), CommentToken(), CommentToken(), CommentToken(), CommentToken()]) 
c1_0: '#srv2:\n' 4 

所以,你“只”,如果有您注释掉第一个键值对创建第一个类型的注释(c0),你必须创建其他(c1)如果您注释掉任何行吟诗人r键值对。 startmarkStreamMark()(来自ruamel/yaml/error.py),创建注释时该实例的唯一重要属性为column

幸运的是,上面显示的过程稍微简单一些,因为没有必要将注释附加到值volumes的“结尾”,将它们附加到值为srv1的末尾具有相同的效果。

在下面的comment_block需要一个键列表,它是要注释的元素的路径。

import sys 
from copy import deepcopy 
from ruamel.yaml import round_trip_dump 
from ruamel.yaml.util import load_yaml_guess_indent 
from ruamel.yaml.error import StreamMark 
from ruamel.yaml.tokens import CommentToken 


yaml_str = """\ 
version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
volumes: 
    some-volume: 
""" 


def comment_block(d, key_index_list, ind, bsi): 
    parent = d 
    for ki in key_index_list[:-1]: 
     parent = parent[ki] 
    # don't just pop the value for key_index_list[-1] that way you lose comments 
    # in the original YAML, instead deepcopy and delete what is not needed 
    data = deepcopy(parent) 
    keys = list(data.keys()) 
    found = False 
    previous_key = None 
    for key in keys: 
     if key != key_index_list[-1]: 
      if not found: 
       previous_key = key 
      del data[key] 
     else: 
      found = True 
    # now delete the key and its value 
    del parent[key_index_list[-1]] 
    if previous_key is None: 
     if parent.ca.comment is None: 
      parent.ca.comment = [None, []] 
     comment_list = parent.ca.comment[1] 
    else: 
     comment_list = parent[previous_key].ca.end = [] 
     parent[previous_key].ca.comment = [None, None] 
    # startmark can be the same for all lines, only column attribute is used 
    start_mark = StreamMark(None, None, None, ind * (len(key_index_list) - 1)) 
    for line in round_trip_dump(data, indent=ind, block_seq_indent=bsi).splitlines(True): 
     comment_list.append(CommentToken('#' + line, start_mark, None)) 

for srv in ['srv1', 'srv2']: 
    data, indent, block_seq_indent = load_yaml_guess_indent(yaml_str) 
    comment_block(data, ['services', srv], ind=indent, bsi=block_seq_indent) 
    round_trip_dump(data, sys.stdout, 
        indent=indent, block_seq_indent=block_seq_indent, 
        explicit_end=True, 
    ) 

它打印:

version: '2' 
services: 
    #srv1: 
    # image: alpine 
    # container_name: srv1 
    # volumes: 
    #  - some-volume:/some/path 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
volumes: 
    some-volume: 
... 
version: '2' 
services: 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume:/some/path 
    #srv2: 
    # image: alpine 
    # container_name: srv2  # second container 
    # volumes_from: 
    #  - some-volume 
volumes: 
    some-volume: 
... 

(该explicit_end=True是没有必要的,这里用来获取两个YAML之间的一些界限自动转储)。

以这种方式删除评论也可以完成。递归搜索注释属性(.ca)以查找注释掉的候选人(可能会提供关于从何处开始的提示)。从注释中去除前导#并连接,然后round_trip_load。根据注释列,您可以确定在哪里附加未注释的键值对。

+0

我的样本输出是严格通过4个空格缩进,它的怪异,为什么它打印6您的浏览器中有空格。 – cherrot

+0

@cherrot在'some-volume:'之前不是,在它们之前有6个短划线偏移4的缩进(即块顺序缩进)。这当然是你如何计算的,但是像'-a'这样的序列元素被计数为缩进2,偏移量为0.这就是'某个音量'的's'比'v'的'v'卷“,这是计为6 indents – Anthon

+0

@cherrot这不是我想出的,这是PyYAML只有一个”缩进“控制的映射和序列中的短划线不计数的结果。我曾经考虑过把它分成两个参数ruamel.yaml,但遇到了很多问题。添加'block-sequence-indent'是目前我能做的最好的。 – Anthon

1

添加uncomment_block功能由@安通的回答启发,以及一些增强功能comment_block

from copy import deepcopy 
from ruamel.yaml import round_trip_dump, round_trip_load 
from ruamel.yaml.error import StreamMark 
from ruamel.yaml.tokens import CommentToken 


def comment_block(root, key_hierarchy_list, indent, seq_indent): 
    found = False 
    comment_key = key_hierarchy_list[-1] 
    parent = root 
    for ki in key_hierarchy_list[:-1]: 
     parent = parent[ki] 
    # don't just pop the value for key_hierarchy_list[-1] that way you lose comments 
    # in the original YAML, instead deepcopy and delete what is not needed 
    block_2b_commented = deepcopy(parent) 
    previous_key = None 
    for key in parent.keys(): 
     if key == comment_key: 
      found = True 
     else: 
      if not found: 
       previous_key = key 
      del block_2b_commented[key] 

    # now delete the key and its value, but preserve its preceding comments 
    preceding_comments = parent.ca.items.get(comment_key, [None, None, None, None])[1] 
    del parent[comment_key] 

    if previous_key is None: 
     if parent.ca.comment is None: 
      parent.ca.comment = [None, []] 
     comment_list = parent.ca.comment[1] 
    else: 
     comment_list = parent[previous_key].ca.end = [] 
     parent[previous_key].ca.comment = [None, None] 

    if preceding_comments is not None: 
     comment_list.extend(preceding_comments) 

    # startmark can be the same for all lines, only column attribute is used 
    start_mark = StreamMark(None, None, None, indent * (len(key_hierarchy_list) - 1)) 
    skip = True 
    for line in round_trip_dump(block_2b_commented, indent=indent, block_seq_indent=seq_indent).splitlines(True): 
     if skip: 
      if not line.startswith(comment_key + ':'): 
       continue 
      skip = False 
     comment_list.append(CommentToken('#' + line, start_mark, None)) 

    return False 


def uncomment_block(root, key_hierarchy_list, indent, seq_indent): 
    ''' 
    FIXME: comments may be attached to the parent's neighbour 
    in document like the following. (srv2 block is attached by volumes, not servies, not srv1). 
    version: '2' 
     services: 
      srv1: foobar 
      #srv2: 
      # image: alpine 
      # container_name: srv2 
      # volumes_from: 
      #  - some-volume 
     volumes: 
      some-volume: 
    ''' 
    found = False 
    parent = root 
    commented_key = key_hierarchy_list[-1] 
    comment_indent = indent * (len(key_hierarchy_list) - 1) 
    for ki in key_hierarchy_list[:-1]: 
     parent = parent[ki] 

    if parent.ca.comment is not None: 
     comment_list = parent.ca.comment[1] 
     found, start, stop = _locate_comment_boundary(comment_list, commented_key, comment_indent) 

    if not found: 
     for key in parent.keys(): 
      bro = parent[key] 
      while hasattr(bro, 'keys') and bro.keys(): 
       bro = bro[bro.keys()[-1]] 

      if not hasattr(bro, 'ca'): 
       continue 

      comment_list = bro.ca.end 
      found, start, stop = _locate_comment_boundary(comment_list, commented_key, comment_indent) 

    if found: 
     block_str = u'' 
     commented = comment_list[start:stop] 
     for ctoken in commented: 
      block_str += ctoken.value.replace('#', '', 1) 
     del(comment_list[start:stop]) 

     block = round_trip_load(block_str) 
     parent.update(block) 
    return found 


def _locate_comment_boundary(comment_list, commented_key, comment_indent): 
    found = False 
    start_idx = 0 
    stop_idx = len(comment_list) 
    for idx, ctoken in enumerate(comment_list): 
     if not found: 
      if ctoken.start_mark.column == comment_indent\ 
        and ctoken.value.replace('#', '', 1).startswith(commented_key): 
       found = True 
       start_idx = idx 
     elif ctoken.start_mark.column != comment_indent: 
      stop_idx = idx 
      break 
    return found, start_idx, stop_idx 


if __name__ == "__main__": 
    import sys 
    from ruamel.yaml.util import load_yaml_guess_indent 

    yaml_str = """\ 
version: '2' 
services: 
    # 1 indent after services 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
volumes: 
    some-volume: 
""" 

    for srv in ['srv1', 'srv2']: 
     # Comment a service block 
     yml, indent, block_seq_indent = load_yaml_guess_indent(yaml_str) 
     comment_block(yml, ['services', srv], indent=indent, seq_indent=block_seq_indent) 
     commented = round_trip_dump(
      yml, indent=indent, block_seq_indent=block_seq_indent, explicit_end=True, 
     ) 
     print(commented) 

     # Now uncomment it 
     yml, indent, block_seq_indent = load_yaml_guess_indent(commented) 
     uncomment_block(yml, ['services', srv], indent=indent, seq_indent=block_seq_indent) 

     round_trip_dump(
      yml, sys.stdout, indent=indent, block_seq_indent=block_seq_indent, explicit_end=True, 
     ) 

输出:

version: '2' 
services: 
    # 1 indent after services 
    #srv1: 
    # image: alpine 
    # container_name: srv1 
    # volumes: 
    #  - some-volume 
    #  # some comments 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
volumes: 
    some-volume: 
... 

version: '2' 
services: 
    # 1 indent after services 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
volumes: 
    some-volume: 
... 
version: '2' 
services: 
    # 1 indent after services 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
    #srv2: 
    # image: alpine 
    # container_name: srv2  # second container 
    # volumes_from: 
    #  - some-volume 
    #  # 2 indent after srv2 volume 
    ## 0 indent before volumes 
volumes: 
    some-volume: 
... 

version: '2' 
services: 
    # 1 indent after services 
    srv1: 
     image: alpine 
     container_name: srv1 
     volumes: 
      - some-volume 
     # some comments 
    srv2: 
     image: alpine 
     container_name: srv2 # second container 
     volumes_from: 
      - some-volume 
     # 2 indent after srv2 volume 
# 0 indent before volumes 
volumes: 
    some-volume: 
...