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
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())
which gives:
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
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__
.
__setitem__(self, ...)
? – Cafard