使用 ruamel.yaml 在嵌套字典中修改其值时如何保留 yaml 文件的注释

How can I keep comments of a yaml file when modify its values in nested dictionaries using ruamel.yaml

我在 的相关答案中使用解决方案,它使用默认(往返)loader/dumper。

我相信,这是一个难题,因为附加 dict 也应该保留评论。

=> 如果我们使用 modify values in nested dictionaries using ruamel.yaml 方法是否可以防止评论被删除?


示例:

config.yaml:

c:  # my comment
  b:  
   f: 5  
a:
  z: 4
  b: 4  # my comment

代码(来自 How to auto-dump modified values in nested dictionaries using ruamel.yaml 的相同代码 ),更改为使用默认值 (round-trip) loader/dumper:

#!/usr/bin/env python3

from pathlib import Path
from ruamel.yaml import YAML, representer


class SubConfig(dict):
    def __init__(self, parent):
        self.parent = parent

    def updated(self):
        self.parent.updated()

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            v = SubConfig(self)
            v.update(value)
            value = v
        super().__setitem__(key, value)
        self.updated()

    def __getitem__(self, key):
        try:
            res = super().__getitem__(key)
        except KeyError:
            super().__setitem__(key, SubConfig(self))
            self.updated()
            return super().__getitem__(key)
        return res

    def __delitem__(self, key):
        res = super().__delitem__(key)
        self.updated()

    def update(self, *args, **kw):
        for arg in args:
            for k, v in arg.items():
                self[k] = v
        for k, v in kw.items():
            self[k] = v
        self.updated()
        return


_SR = representer.RoundTripRepresenter
_SR.add_representer(SubConfig, _SR.represent_dict)


class Config(dict):
    def __init__(self, filename, auto_dump=True):
        self.filename = filename if hasattr(filename, "open") else Path(filename)
        self.auto_dump = auto_dump
        self.changed = False
        self.yaml = YAML()
        self.yaml.indent(mapping=4, sequence=4, offset=2)
        self.yaml.default_flow_style = False
        if self.filename.exists():
            with open(filename) as f:
                self.update(self.yaml.load(f) or {})

    def updated(self):
        if self.auto_dump:
            self.dump(force=True)
        else:
            self.changed = True

    def dump(self, force=False):
        if not self.changed and not force:
            return
        with open(self.filename, "w") as f:
            self.yaml.dump(dict(self), f)
        self.changed = False

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            v = SubConfig(self)
            v.update(value)
            value = v
        super().__setitem__(key, value)
        self.updated()

    def __getitem__(self, key):
        try:
            res = super().__getitem__(key)
        except KeyError:
            super().__setitem__(key, SubConfig(self))
            self.updated()
        return super().__getitem__(key)

    def __delitem__(self, key):
        res = super().__delitem__(key)
        self.updated()

    def update(self, *args, **kw):
        for arg in args:
            for k, v in arg.items():
                self[k] = v
        for k, v in kw.items():
            self[k] = v
        self.updated()


cfg = Config(Path("config.yaml"))

=> config.yaml 文件更新如下,删除了注释:

c:
    b:
        f: 5
a:
    z: 4
    b: 4

是的,可以防止评论丢失。在你的 Config.__init__() 方法中从 self.yaml.load() 返回的对象不是一个 dict,而是一个包含所有注释信息的子 class (ruamel.yaml.comments.CommentedMap) (在它的 .ca 属性。并且 CommentedMap 将再次具有自身的值 CommentedMap 个实例(至少在您的输入中。

所以你需要做的是改变你的 classes:

class Config(ruamel.yaml.comments.CommentedMap):

并对 SubConfig 执行相同的操作。然后在 update 例程中,您应该尝试复制 .ca 属性(它将在 CommentedMap 上创建为空,但在 {} 上不可用)

确保你们都为 Config 添加了代表,并且 不要 Config.dump() 方法中转换为 dict


如果你也复制加载数据的 .fa 属性(也在 Subconfig 上),你将保留原始的 flow/block 风格,你可以做一个远离 self.yaml.default_flow_style = False.


以上是理论,实践中还有几个问题。

您的 config.yaml 更改,尽管您没有明确转储。那是 因为您的 auto_dump 默认为 True。但这也意味着 每次更改都会转储,即您的 config.yaml 被转储 10 (ten) 次,而 Config/SubConfig 数据结构得到构建。 如果 auto_dumpTrue,我将其更改为仅转储一次,但即便如此我也不会 推荐,而不是仅在加载后更改时转储。

A dict 不需要初始化,但是 CommentedMap 需要。 如果你不这样做,你会在某个时候得到一个属性错误。所以你必须 在每个 class.

__init__ 中调用 super().__init__(self)
from pathlib import Path
import ruamel.yaml

_SR = ruamel.yaml.representer.RoundTripRepresenter

class SubConfig(ruamel.yaml.comments.CommentedMap):
    def __init__(self, parent):
        self.parent = parent
        super().__init__(self)

    def updated(self):
        self.parent.updated()

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            v = SubConfig(self)
            v.update(value)
            value = v
        super().__setitem__(key, value)
        self.updated()

    def __getitem__(self, key):
        try:
            res = super().__getitem__(key)
        except KeyError:
            super().__setitem__(key, SubConfig(self))
            self.updated()
            return super().__getitem__(key)
        return res

    def __delitem__(self, key):
        res = super().__delitem__(key)
        self.updated()

    def update(self, *args, **kw):
        for arg in args:
            for k, v in arg.items():
                self[k] = v
            for attr in [ruamel.yaml.comments.Comment.attrib, ruamel.yaml.comments.Format.attrib]:
                if hasattr(arg, attr):
                    setattr(self, attr, getattr(arg, attr))
        for k, v in kw.items():
            self[k] = v
        self.updated()
        return


_SR.add_representer(SubConfig, _SR.represent_dict)


class Config(ruamel.yaml.comments.CommentedMap):
    def __init__(self, filename, auto_dump=True):
        super().__init__(self)
        self.filename = filename if hasattr(filename, "open") else Path(filename)
        self.auto_dump = False  # postpone setting during loading of config
        self.changed = False
        self.yaml = ruamel.yaml.YAML()
        self.yaml.indent(mapping=4, sequence=4, offset=2)
        # self.yaml.default_flow_style = False
        if self.filename.exists():
            with open(filename) as f:
                self.update(self.yaml.load(f) or {})
        self.auto_dump = auto_dump
        if auto_dump:
            self.dump()

    def updated(self):
        if self.auto_dump:
            self.dump(force=True)
        else:
            self.changed = True

    def dump(self, force=False):
        if not self.changed and not force:
            return
        # use the capability of dump to take a Path. It will open the file 'wb' as
        # is appropriate for a YAML file, which is UTF-8
        self.yaml.dump(self, self.filename)
        self.changed = False

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            v = SubConfig(self)
            v.update(value)
            value = v
        super().__setitem__(key, value)
        self.updated()

    def __getitem__(self, key):
        try:
            res = super().__getitem__(key)
        except KeyError:
            super().__setitem__(key, SubConfig(self))
            self.updated()
        return super().__getitem__(key)

    def __delitem__(self, key):
        res = super().__delitem__(key)
        self.updated()

    def update(self, *args, **kw):
        for arg in args:
            for k, v in arg.items():
                self[k] = v
            for attr in [ruamel.yaml.comments.Comment.attrib, ruamel.yaml.comments.Format.attrib]:
                if hasattr(arg, attr):
                    setattr(self, attr, getattr(arg, attr))
        for k, v in kw.items():
            self[k] = v
        self.updated()

_SR.add_representer(Config, _SR.represent_dict)

fn = Path('config.yaml')
fn.write_text("""
c:  # my comment
  b:
     f: 5
  x: {g: 6}
a:
  z: 4
  b: 4  # my comment
""")
cfg = Config(fn)
print(Path(fn).read_text())

给出:

c:  # my comment
    b:
        f: 5
    x: {g: 6}
a:
    z: 4
    b: 4 # my comment

由于输入发生变化,我编写了配置文件以在每个 运行 上进行测试。 我还添加了一个流式字典,以明确执行原始格式。