如何在Python中向YAML文件添加注释

10

我正在使用 https://pypi.python.org/pypi/ruamel.yaml 编写一个 YAML 文件。

代码如下:

import ruamel.yaml
from ruamel.yaml.comments import CommentedSeq

d = {}
for m in ['B1', 'B2', 'B3']:
    d2 = {}
    for f in ['A1', 'A2', 'A3']:
        d2[f] = CommentedSeq(['test', 'test2'])
        if f != 'A2':
            d2[f].fa.set_flow_style()
    d[m] = d2

    with open('test.yml', "w") as f:
        ruamel.yaml.dump(
            d, f, Dumper=ruamel.yaml.RoundTripDumper,
            default_flow_style=False, width=50, indent=8)

我只是想在顶部添加注释,类似于:

# Data for Class A

在YAML数据之前。


略微相关:*如何在YAML中进行块注释?* - Peter Mortensen
2个回答

11

在您的with块内,您可以将任何内容写入文件。由于您只需要在顶部添加注释,请在调用ruamel之前添加对f.write()的调用:

with open('test.yml', "w") as f:
    f.write('# Data for Class A\n')
    ruamel.yaml.dump(
        d, f, Dumper=ruamel.yaml.RoundTripDumper,
        default_flow_style=False, width=50, indent=8)

4
原则上是可以实现的,因为您可以往返这样的“文件开头”注释,但在当前的ruamel.yaml 0.10中不支持得很好,特别是从头开始(即不改变现有文件)。下面是一个简单而相对不错的解决方案,但我想先介绍一个丑陋的解决方法以及逐步完成此操作的步骤。
丑陋的方式: 要做到这一点的丑陋方法是,在将YAML数据写入文件之前,只需将注释添加到文件中。也就是说,插入:
f.write('# Data for Class A\n')

ruamel.yaml.dump(...)之前:

步骤
为了在数据结构中插入注释,以避免上述的hack方法,您首先需要确保您的d数据是CommentedMap类型。如果您将带有注释的YAML加载回c后,将d变量与具有注释的变量进行比较,您会发现它们之间的差异。

import ruamel.yaml
from ruamel.yaml.comments import Comment, CommentedSeq, CommentedMap

d = CommentedMap()             # <<<<< most important
for m in ['B1', 'B2', 'B3']:
    d2 = {}
    for f in ['A1', 'A2', 'A3']:
        d2[f] = CommentedSeq(['test', 'test2'])
        if f != 'A2':
            d2[f].fa.set_flow_style()
    d[m] = d2

yaml_str = ruamel.yaml.dump(d, Dumper=ruamel.yaml.RoundTripDumper,
                            default_flow_style=False, width=50, indent=8)

assert not hasattr(d, Comment.attrib)  # no attribute on the CommentedMap

comment = 'Data for Class A'
commented_yaml_str = '# ' + comment + '\n' + yaml_str
c = ruamel.yaml.load(commented_yaml_str, Loader=ruamel.yaml.RoundTripLoader)
assert hasattr(c, Comment.attrib)  # c has the attribute
print c.ca                         # and this is what it looks like
print d.ca                         # accessing comment attribute creates it empty
assert hasattr(d, Comment.attrib)  # now the CommentedMap has the attribute

这将打印:

Comment(comment=[None, [CommentToken(value=u'# Data for Class A\n')]],
  items={})
Comment(comment=None,
  items={})

一个Comment有一个属性comment,需要设置为一个由EOL注释(始终只有一个)和前面行注释的列表(以CommentTokens的形式)组成的2个元素列表。

要创建一个CommentToken,你需要一个(虚假的)起始标记,告诉它从哪一列开始:

from ruamel.yaml.error import StreamMark
start_mark = StreamMark(None, None, None, 0, None, None)  # column 0

现在您可以创建令牌:
from ruamel.yaml.tokens import CommentToken

ct = CommentToken('# ' + comment + '\n', start_mark, None)

在您的CommentedMap中,将令牌分配为前一个列表的第一个元素:

d.ca.comment = [None, [ct]]
print d.ca   # in case you want to check

给你:
Comment(comment=[None, [CommentToken(value='# Data for Class A\n')]],
  items={})

最后:
print ruamel.yaml.dump(d, Dumper=ruamel.yaml.RoundTripDumper)  

gives:

# Data for Class A
B1:
        A1: [test, test2]
        A3: [test, test2]
        A2:
        - test
        - test2
B2:
        A1: [test, test2]
        A3: [test, test2]
        A2:
        - test
        - test2
B3:
        A1: [test, test2]
        A3: [test, test2]
        A2:
        - test
        - test2

当然,你不需要创建c对象,这只是为了说明。

应该使用什么:为了使整个练习变得更容易,您可以忘记细节,并将以下方法补丁到CommentedBase中:

from ruamel.yaml.comments import CommentedBase

def set_start_comment(self, comment, indent=0):
    """overwrites any preceding comment lines on an object
    expects comment to be without `#` and possible have mutlple lines
    """
    from ruamel.yaml.error import StreamMark
    from ruamel.yaml.tokens import CommentToken
    if self.ca.comment is None:
        pre_comments = []
        self.ca.comment = [None, pre_comments]
    else:
        pre_comments = self.ca.comments[1]
    if comment[-1] == '\n':
        comment = comment[:-1]  # strip final newline if there
    start_mark = StreamMark(None, None, None, indent, None, None)
    for com in comment.split('\n'):
        pre_comments.append(CommentToken('# ' + com + '\n', start_mark, None))

if not hasattr(CommentedBase, 'set_start_comment'): # in case it is there
    CommentedBase.set_start_comment = set_start_comment

然后只需要执行:

d.set_start_comment('Data for Class A')

1
这似乎是很多工作,只是为了解决调用f.write()的问题。有什么好处吗?话虽如此,看起来它作为ruamel的内置部分是有意义的,也许可以向他们发送一个pull请求? - dimo414
2
@dimo414 如果只是放在文件顶部,那么它就是额外的开销。但如果将其添加到数据中,然后在列表中使用数据并将其写出,它就可以工作了。我想以通用的方式解决这个问题,并将其包含在ruamel.yaml中,不需要PR,因为我是作者。因此,它将包含在我上传到PyPI(10.1)的下一个更新中。然后我可以将答案缩减为仅最后一行;-) - Anthon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接