cherrot cherrot - 5 months ago 33
Python Question

How to comment out a YAML section using ruamel.yaml?

Recently I was trying to manage my docker-compose service configuration (namely

docker-compose.yml
) using
ruamel.yaml
.

I need to comment out & uncomment a service block when needed. Suppose I have the following file:

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:


Is there some workaround to comment out the srv2 block? Like the following output:

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:


Moreover, is there a way to uncomment this block? (Suppose I have already hold the original
srv2
block, I just need a method to delete these commented lines)

Answer Source

If srv2 is a key that is unique for all of the mappings in your YAML, then the "easy" way is to loop over de lines, test if de stripped version of the line starts with srv2:, note the number of leading spaces and comment out that and following lines until you notice a line that has equal or less leading spaces. The advantage of doing that, apart from being simple and fast is that it can deal with irregular indentation (as in your example: 4 positions before srv1 and 6 before some-volume).

Doing this on the using ruamel.yaml is possible as well, but less straightforward. You have to know that when round_trip_loading, ruamel.yaml normally attaches a comment to the last structure (mapping/sequence) that has been processed and that as a consequence of that commenting out srv1 in your example works completely different from srv2 (i.e. the first key-value pair, if commented out, differs from all the other key-value pairs).

If you normalize your expected output to four positions indent and add a comment before srv1 for analysis purposes, load that, you can search for where the comment ends up:

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)

which prints:

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

So you "only", have to create the first type comment (c0) if you comment out the first key-value pair and you have to create the other (c1) if you comment out any other key-value pair. The startmark is a StreamMark() (from ruamel/yaml/error.py) and the only important attribute of that instance when creating comments is column.

Fortunately this is made slightly easier then shown above, as it is not necessary to attach the comments to the "end" of the value of volumes, attaching them to the end of the value of srv1 has the same effect.

In the following comment_block expects a list of keys that is the path to the element to be commented out.

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,
    )

which prints:

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:
...

(the explicit_end=True is not necessary, it is used here to get some demarcation between the two YAML dumps automatically).

Removing the comments this way can be done as well. Recursively search the comment attributes (.ca) for a commented out candidate (maybe giving some hints on where to start). Strip the leading # from the comments and concatenate, then round_trip_load. Based on the column of the comments you can determine where to attach the uncommented key-value pair.