如何使用 ruamel.yaml 自动转储嵌套字典中的修改值?

How to auto-dump modified values in nested dictionaries using ruamel.yaml?

当我尝试遵循解决方案 PyYAML - Saving data to .yaml files 并尝试使用 ruamel.yaml

修改嵌套字典中的值时
cfg = Config("test.yaml")
cfg['setup']['a'] = 3 
print(cfg)  # I can see the change for the `dict` but it is not saved

cfg['setup']['a'] 值已更改,但未被 __setitem__() 捕获且未使用 updated() 函数保存。

是否可以自动转储嵌套 dict 中值的任何修改更改?

例如:

PyYAML - Saving data to .yaml files:


class Config(dict):
    def __init__(self, filename, auto_dump=True):
        self.filename = filename
        self.auto_dump = auto_dump
        self.changed = False
        self.yaml = YAML()
        self.yaml.preserve_quotes = True
        if os.path.isfile(filename):
            with open(filename) as f:
                super(Config, self).update(self.yaml.load(f) or {})

    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 updated(self):
        if self.auto_dump:
            self.dump(force=True)
        else:
            self.changed = True

    def __setitem__(self, key, value):
        super(Config, self).__setitem__(key, value)
        self.updated()

    def update(self, *args, **kw):
        for arg in args:
            super(Config, self).update(arg)
        super(Config, self).update(**kw)
        self.updated()

相关:

您需要制作一个行为类似于 Config 的辅助 class SubConfig。 在此之前摆脱旧样式 super(Config, self) 可能是个好主意。

更改__setitem__以检查该值是否为字典,如果是 实例化 SubConfig 然后设置各个项目( SubConfig 也需要这样做,所以你可以有任意嵌套)。

__init__ 上的 SubConfig 不采用文件名,但采用 父级(ConfigSubConfig 类型)。 Subconfig 本身不应该 dump,它的 updated 应该调用父 updated (最终 冒泡到 Config 然后进行保存)。

为了支持 cfg['a'] = dict(c=1),您需要实施 __getitem__,并且 类似于 del cfg['a'] 实现 __delitem__,使其写入更新的文件。

我认为你可以从另一个文件中提取class一个文件,因为几种方法是相同的, 但无法使其与 super() 一起正常工作。

如果您曾将列表分配给(嵌套的)键,并希望在更新元素时​​自动转储 在这样的列表中,您需要实现一些 SubConfigList 并处理 __setitem__

中的那些
import sys
import os
from pathlib import Path
import ruamel.yaml

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 = ruamel.yaml.representer.SafeRepresenter
_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 = ruamel.yaml.YAML(typ='safe')
        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()

config_file = Path('config.yaml') 

cfg = Config(config_file)
cfg['a'] = 1
cfg['b']['x'] = 2
cfg['c']['y']['z'] = 42

print(f'{config_file} 1:')
print(config_file.read_text())

cfg['b']['x'] = 3
cfg['a'] = 4

print(f'{config_file} 2:')
print(config_file.read_text())

cfg.update(a=9, d=196)
cfg['c']['y'].update(k=11, l=12)

print(f'{config_file} 3:')
print(config_file.read_text())
        
# reread config from file
cfg = Config(config_file)
assert isinstance(cfg['c']['y'], SubConfig)
assert cfg['c']['y']['z'] == 42
del cfg['c']
print(f'{config_file} 4:')
print(config_file.read_text())


# start from scratch immediately use updating
config_file.unlink()
cfg = Config(config_file)
cfg.update(a=dict(b=4))
cfg.update(c=dict(b=dict(e=5)))
assert isinstance(cfg['a'], SubConfig)
assert isinstance(cfg['c']['b'], SubConfig)
cfg['c']['b']['f'] = 22

print(f'{config_file} 5:')
print(config_file.read_text())

给出:

config.yaml 1:
a: 1
b:
  x: 2
c:
  y:
    z: 42

config.yaml 2:
a: 4
b:
  x: 3
c:
  y:
    z: 42

config.yaml 3:
a: 9
b:
  x: 3
c:
  y:
    k: 11
    l: 12
    z: 42
d: 196

config.yaml 4:
a: 9
b:
  x: 3
d: 196

config.yaml 5:
a:
  b: 4
c:
  b:
    e: 5
    f: 22

您应该考虑不要让这些 class 成为 dict 的子 class,而是将字典作为属性 ._d(并替换 super().self._d.)。这将需要一个特定的代表 function/method.

这样做的好处是您不会意外地获得一些字典功能。例如。在上面的subclassing实现中,如果我没有实现__delitem__,你仍然可以del cfg['c']而不会出错,但是不会自动写入YAML文件。如果 dict 是一个属性,在你实现 __delitem__.

之前你会得到一个错误