首页 > 解决方案 > 如何使用 ruamel.yaml 自动转储嵌套字典中的修改值?

问题描述

当我尝试遵循解决方案PyYAML - 将数据保存到 .yaml 文件并尝试使用 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 - 将数据保存到 .yaml 文件


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

有关的:

标签: pythonruamel.yaml

解决方案


您将需要创建一个SubConfig行为类似于Config. super(Config, self)在此之前摆脱旧样式可能是一个好主意。

更改__setitem__以检查该值是否为 dict,如果是,则实例化SubConfig然后设置各个项目(SubConfig 也需要这样做,因此您可以进行任意嵌套)。

子配置 on__init__不采用文件名,但它采用父级(类型为Configor SubConfig)。Subconfig本身不应该转储,它updated应该调用父母updated(最终冒泡Config然后进行保存)。

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

我认为您可以将一个文件从另一个文件子类化,因为几种方法是相同的,但无法使其正常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

您应该考虑不要使这些类成为 的子类dict,而是将 dict 作为属性._d(并替换super().self._d.)。这将需要一个特定的表示函数/方法。

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


推荐阅读