我正在使用PyYaml从我自己的python对象创建Yaml文档。 例如我的对象:
class MyObj(object):
name = "boby"
age = 34
变为:
boby:
age: 34
到目前为止一切顺利。
但我还没有找到一种方法以编程方式为生成的yaml添加注释,所以它看起来像:
boby: # this is the name
age: 34 # in years
查看PyYaml文档以及代码,我发现没办法这样做。
有什么建议吗?
答案 0 :(得分:5)
你可能有一些MyObj类的代表,因为默认情况下使用PyYAML转储(print(yaml.dump(MyObj()))
)会给你:
!!python/object:__main__.MyObj {}
PyYAML只能对你想要的输出中的注释做一件事:丢弃它们。如果您要重新读取所需的输出,则结束
使用包含字典({'boby': {'age': 34}}
的字典,您将无法获得MyObj()
实例,因为没有标记信息)
我开发的PyYAML增强版(ruamel.yaml)可以在YAML中读取注释,保留注释并在转储时写注释。
如果您读取了所需的输出,结果数据将看起来(和行为)像包含dict的dict,但实际上有更复杂的数据结构可以处理注释。但是,当ruamel.yaml要求您转储MyObj
的实例时,您可以创建该结构,如果您在此时添加注释,您将获得所需的输出。
from __future__ import print_function
import sys
import ruamel.yaml
from ruamel.yaml.comments import CommentedMap
class MyObj():
name = "boby"
age = 34
def convert_to_yaml_struct(self):
x = CommentedMap()
a = CommentedMap()
x[data.name] = a
x.yaml_add_eol_comment('this is the name', 'boby', 11)
a['age'] = data.age
a.yaml_add_eol_comment('in years', 'age', 11)
return x
@staticmethod
def yaml_representer(dumper, data, flow_style=False):
assert isinstance(dumper, ruamel.yaml.RoundTripDumper)
return dumper.represent_dict(data.convert_to_yaml_struct())
ruamel.yaml.RoundTripDumper.add_representer(MyObj, MyObj.yaml_representer)
ruamel.yaml.round_trip_dump(MyObj(), sys.stdout)
打印哪些:
boby: # this is the name
age: 34 # in years
在您想要表示CommentedMap
实例之前,无需等待创建MyObj
实例。我会...将name
和age
设置为从适当的CommentedMap
获取/设置值的属性。这样,您可以在调用yaml_representer
静态方法之前更轻松地添加注释,以表示MyObj
实例。
答案 1 :(得分:3)
这是我想出的解决方案;它比ruamel稍微复杂一点,但是却不那么复杂,因为它完全可以与普通的PyYAML API一起使用,并且不会来回注释(因此这不是对this other question的适当回答)。总体而言,它可能还不那么健壮,因为我还没有进行广泛的测试,但是对于我的用例来说,这似乎已经足够了,这是我希望dict / mapping能够对整个映射以及整个映射都具有注释。每个项目的评论。
我认为,在这种有限的情况下,使用类似的方法也可以进行往返注释,但是我没有尝试过,因为目前还没有用例。
最后,虽然此解决方案未实现向列表/序列中的项目添加逐项注释(因为目前我不需要),但可以轻松扩展此方法。
首先,如ruamel一样,我们需要一种CommentedMapping
类,该类将注释与Mapping中的每个键相关联。有许多可能的方法可以解决这个问题。我的只是一个:
from collections.abc import Mapping, MutableMapping
class CommentedMapping(MutableMapping):
def __init__(self, d, comment=None, comments={}):
self.mapping = d
self.comment = comment
self.comments = comments
def get_comment(self, *path):
if not path:
return self.comment
# Look the key up in self (recursively) and raise a
# KeyError or other execption if such a key does not
# exist in the nested structure
sub = self.mapping
for p in path:
if isinstance(sub, CommentedMapping):
# Subvert comment copying
sub = sub.mapping[p]
else:
sub = sub[p]
comment = None
if len(path) == 1:
comment = self.comments.get(path[0])
if comment is None:
comment = self.comments.get(path)
return comment
def __getitem__(self, item):
val = self.mapping[item]
if (isinstance(val, (dict, Mapping)) and
not isinstance(val, CommentedMapping)):
comment = self.get_comment(item)
comments = {k[1:]: v for k, v in self.comments.items()
if isinstance(k, tuple) and len(k) > 1 and k[0] == item}
val = self.__class__(val, comment=comment, comments=comments)
return val
def __setitem__(self, item, value):
self.mapping[item] = value
def __delitem__(self, item):
del self.mapping[item]
for k in list(self.comments):
if k == item or (isinstance(k, tuple) and k and k[0] == item):
del self.comments[key]
def __iter__(self):
return iter(self.mapping)
def __len__(self):
return len(self.mapping)
def __repr__(self):
return f'{type(self).__name__}({self.mapping}, comment={self.comment!r}, comments={self.comments})'
该类具有.comment
属性,以便可以承载映射的整体注释,还包含包含每个键注释的.comments
属性。通过将键路径指定为元组,它还允许为嵌套字典中的键添加注释。例如。 comments={('c', 'd'): 'comment'}
允许在'd'
的嵌套字典中为键'c'
指定注释。从CommentedMapping
获取项目时,如果项目的值为dict / Mapping,它也将以保留其注释的方式包装在CommentedMapping
中。这对于递归调用嵌套结构的YAML表示符很有用。
接下来,我们需要实现一个自定义的YAML Dumper,它负责将对象序列化为YAML的整个过程。自卸车是一个复杂的类,它由四个其他类组成:Emitter
,Serializer
,Representer
和Resolver
。其中,我们只需要执行前三个即可。 Resolver
更关注,例如1
等隐式标量如何解析为正确的类型,以及如何确定各种值的默认标记。这里没有真正涉及。
首先,我们实现一个 resolver 。解析程序负责识别不同的Python类型,并将它们映射到本机YAML数据结构/表示图中的相应 nodes 。即,这些包括标量节点,序列节点和映射节点。例如,基础Representer
类包括Python dict
的表示符,该表示符将它们转换为MappingNode
(字典中的每个项目又由一对ScalarNode
组成) s,每个键一个,每个值一个。
为了将注释附加到整个映射以及映射中的每个键,我们引入了两种新的Node
类型,它们并不是YAML规范的正式组成部分:
from yaml.node import Node, ScalarNode, MappingNode
class CommentedNode(Node):
"""Dummy base class for all nodes with attached comments."""
class CommentedScalarNode(ScalarNode, CommentedNode):
def __init__(self, tag, value, start_mark=None, end_mark=None, style=None,
comment=None):
super().__init__(tag, value, start_mark, end_mark, style)
self.comment = comment
class CommentedMappingNode(MappingNode, CommentedNode):
def __init__(self, tag, value, start_mark=None, end_mark=None,
flow_style=None, comment=None, comments={}):
super().__init__(tag, value, start_mark, end_mark, flow_style)
self.comment = comment
self.comments = comments
然后,我们添加一个CommentedRepresenter
,其中包括将CommentedMapping
表示为CommentedMappingNode
的代码。实际上,它只是重用基类的代码来表示映射,而是将返回的MappingNode
转换为CommentedMappingNode
。还将每个密钥从ScalarNode
转换为CommentedscalarNode
。我们这里基于SafeRepresenter
,因为我不需要序列化任意Python对象:
from yaml.representer import SafeRepresenter
class CommentedRepresenter(SafeRepresenter):
def represent_commented_mapping(self, data):
node = super().represent_dict(data)
comments = {k: data.get_comment(k) for k in data}
value = []
for k, v in node.value:
if k.value in comments:
k = CommentedScalarNode(
k.tag, k.value,
k.start_mark, k.end_mark, k.style,
comment=comments[k.value])
value.append((k, v))
node = CommentedMappingNode(
node.tag,
value,
flow_style=False, # commented dicts must be in block style
# this could be implemented differently for flow-style
# maps, but for my case I only want block-style, and
# it makes things much simpler
comment=data.get_comment(),
comments=comments
)
return node
yaml_representers = SafeRepresenter.yaml_representers.copy()
yaml_representers[CommentedMapping] = represent_commented_mapping
接下来,我们需要实现Serializer
的子类。 serializer 负责遍历节点的表示图,并为每个节点向 emitter 输出一个或多个事件,这很复杂(有时很难遵循)状态机,它接收事件流并为每个事件输出适当的YAML标记(例如,存在一个MappingStartEvent
,如果它是流样式的映射,则将收到{
,并且/或为后续输出增加适当的缩进级别,直到相应的MappingEndEvent
。
CommentEvent
并在表示中每次遇到CommentedMappingNode
或CommentedScalarNode
时发出它们来进行处理:
from yaml import Event
class CommentEvent(yaml.Event):
"""
Simple stream event representing a comment to be output to the stream.
"""
def __init__(self, value, start_mark=None, end_mark=None):
super().__init__(start_mark, end_mark)
self.value = value
class CommentedSerializer(Serializer):
def serialize_node(self, node, parent, index):
if (node not in self.serialized_nodes and
isinstance(node, CommentedNode) and
not (isinstance(node, CommentedMappingNode) and
isinstance(parent, CommentedMappingNode))):
# Emit CommentEvents, but only if the current node is not a
# CommentedMappingNode nested in another CommentedMappingNode (in
# which case we would have already emitted its comment via the
# parent mapping)
self.emit(CommentEvent(node.comment))
super().serialize_node(node, parent, index)
接下来,Emitter
需要被子类化以处理CommentEvent
。这可能是最棘手的部分,因为在我写发射器时,它有点复杂和脆弱,并且编写时很难修改状态机(我很想更清楚地重写它,但是没有时间)马上)。因此,我尝试了许多不同的解决方案。
这里的关键方法是Emitter.emit
,它处理事件流,并调用“状态”方法,这些方法根据计算机所处的状态来执行某些操作,该状态又受流中出现的事件的影响。一个重要的认识是,在许多情况下,在等待更多事件进入时,流处理被暂停了,这就是Emitter.need_more_events
方法所负责的。在某些情况下,在可以处理当前事件之前,需要先输入更多事件。例如,在MappingStartEvent
的情况下,流上至少还需要缓冲3个事件:第一个键/值对,以及可能的下一个键。发射器必须先知道映射中是否存在一个或多个项目,然后才能开始格式化映射,还需要知道第一个键/值对的长度。在need_more_events
方法中,可以硬编码当前事件之前需要处理的事件数。
问题在于,这不能说明事件流上现在可能存在CommentEvent
,这不会影响其他事件的处理。因此,使用Emitter.need_events
方法来解决CommentEvent
的存在。例如。如果当前事件是MappingStartEvent
,并且缓冲了3个后续事件,那么如果其中一个是CommentEvent
,我们就无法计数,因此至少需要4个事件(以防万一下一个是映射中的预期事件之一。
最后,每次在流上遇到CommentEvent
时,我们都会强行脱离当前事件处理循环来处理写注释,然后将CommentEvent
弹出流并继续,就像什么都没发生。这是最终结果:
import textwrap
from yaml.emitter import Emitter
class CommentedEmitter(Emitter):
def need_more_events(self):
if self.events and isinstance(self.events[0], CommentEvent):
# If the next event is a comment, always break out of the event
# handling loop so that we divert it for comment handling
return True
return super().need_more_events()
def need_events(self, count):
# Hack-y: the minimal number of queued events needed to start
# a block-level event is hard-coded, and does not account for
# possible comment events, so here we increase the necessary
# count for every comment event
comments = [e for e in self.events if isinstance(e, CommentEvent)]
return super().need_events(count + min(count, len(comments)))
def emit(self, event):
if self.events and isinstance(self.events[0], CommentEvent):
# Write the comment, then pop it off the event stream and continue
# as normal
self.write_comment(self.events[0].value)
self.events.pop(0)
super().emit(event)
def write_comment(self, comment):
indent = self.indent or 0
width = self.best_width - indent - 2 # 2 for the comment prefix '# '
lines = ['# ' + line for line in wrap(comment, width)]
for line in lines:
if self.encoding:
line = line.encode(self.encoding)
self.write_indent()
self.stream.write(line)
self.write_line_break()
我还尝试了不同的方法来实现write_comment
。 Emitter
基类具有自己的方法(write_plain
),该方法可以处理带有适当的缩进和换行的文本写入流。但是,它不够灵活,无法处理诸如注释之类的注释,在注释中,每行都必须以'# '
之类的前缀。我尝试过的一种技术是用猴子修补write_indent
方法来处理这种情况,但最终它太难看了。我发现仅使用Python内置的textwrap.wrap
就足够了。
接下来,我们将现有的SafeDumper
子类化,然后将新的类插入MRO中,以创建转储程序:
from yaml import SafeDumper
class CommentedDumper(CommentedEmitter, CommentedSerializer,
CommentedRepresenter, SafeDumper):
"""
Extension of `yaml.SafeDumper` that supports writing `CommentedMapping`s with
all comments output as YAML comments.
"""
这是一个用法示例:
>>> import yaml
>>> d = CommentedMapping({
... 'a': 1,
... 'b': 2,
... 'c': {'d': 3},
... }, comment='my commented dict', comments={
... 'a': 'a comment',
... 'b': 'b comment',
... 'c': 'long string ' * 44,
... ('c', 'd'): 'd comment'
... })
>>> print(yaml.dump(d, Dumper=CommentedDumper))
# my commented dict
# a comment
a: 1
# b comment
b: 2
# long string long string long string long string long string long string long
# string long string long string long string long string long string long string
# long string long string long string long string long string long string long
# string long string long string long string long string long string long string
# long string long string long string long string long string long string long
# string long string long string long string long string long string long string
# long string long string long string long string long string
c:
# d comment
d: 3
我还没有对这个解决方案进行非常广泛的测试,它可能仍然包含错误。我将在更新它时对其进行更新,并找到拐角处的情况,等等。