Skip to content Skip to sidebar Skip to footer

How To Auto-dump Modified Values In Nested Dictionaries Using Ruamel.yaml?

When I try following solution PyYAML - Saving data to .yaml files and try to modify values in nested dictionaries using ruamel.yaml cfg = Config('test.yaml') cfg['setup']['a'] = 3

Solution 1:

You will need to make a secondary class SubConfig that behaves similar to Config. It is probably a good idea to get rid of the old style super(Config, self) before that.

Change __setitem__ to check that the value is a dict, and if so instantiate SubConfig and then setting the individual items (the SubConfig needs to do that as well, so you can have arbitrary nesting).

The SubConfig, on __init__, doesn't take a filename, but it takes a parent (of type Config or SubConfig). Subconfig itself shouldn't dump, and its updated should call the parents updated (eventually bubbling up to Config that then does a save).

In order to support doing cfg['a'] = dict(c=1) you need to implement __getitem__, and similar for del cfg['a'] implement __delitem__, to make it write the updated file.

I thought you could subclass one file fromt the other as several methods are the same, but couldn't get this to work with super() properly.

If you ever assign lists to (nested) keys, and want to autodump on updating an element in such a list you'll need to implement some SubConfigList and handle those in __setitem__

import sys
import os
from pathlib import Path
import ruamel.yaml

classSubConfig(dict):
    def__init__(self, parent):
        self.parent = parent

    defupdated(self):
        self.parent.updated()

    def__setitem__(self, key, value):
        ifisinstance(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()
            returnsuper().__getitem__(key)
        return res

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

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

classConfig(dict):
    def__init__(self, filename, auto_dump=True):
        self.filename = filename ifhasattr(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 = Falseif self.filename.exists():
            withopen(filename) as f:
                self.update(self.yaml.load(f) or {})

    defupdated(self):
        if self.auto_dump:
            self.dump(force=True)
        else:
            self.changed = Truedefdump(self, force=False):
        ifnot self.changed andnot force:
            returnwithopen(self.filename, "w") as f:
            self.yaml.dump(dict(self), f)
        self.changed = Falsedef__setitem__(self, key, value):
        ifisinstance(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()
        returnsuper().__getitem__(key)

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

    defupdate(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'] = 42print(f'{config_file} 1:')
print(config_file.read_text())

cfg['b']['x'] = 3
cfg['a'] = 4print(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)
assertisinstance(cfg['c']['y'], SubConfig)
assert cfg['c']['y']['z'] == 42del 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)))
assertisinstance(cfg['a'], SubConfig)
assertisinstance(cfg['c']['b'], SubConfig)
cfg['c']['b']['f'] = 22print(f'{config_file} 5:')
print(config_file.read_text())

which gives:

config.yaml 1:a:1b:x:2c:y:z:42config.yaml 2:a:4b:x:3c:y:z:42config.yaml 3:a:9b:x:3c:y:k:11l:12z:42d:196config.yaml 4:a:9b:x:3d:196config.yaml 5:a:b:4c:b:e:5f:22

You should consider not making these classes a subclass of dict, but have the dict as an attribute ._d (and replace super(). with self._d.). This would require a specific representer function/method.

The advantage of that is that you don't get some dict functionality unexpectedly. E.g. in the above subclassing implementation, if I hadn't implemented __delitem__, you could still do del cfg['c'] without an error, but the YAML file would not be written automatically. If the dict is an attribute, you'll get an error until you implement __delitem__.

Post a Comment for "How To Auto-dump Modified Values In Nested Dictionaries Using Ruamel.yaml?"