Hierarchical ChainMaps
After exploring ChainMaps in Python and Config class using ChainMaps, I have decided to try and write a Hierarchical ChainMap that can be used as a configuration class. This is mainly keeping in mind Matplotlib but might be usable in other cases with a few changes too.
from collections.abc import Mapping, MutableMapping from collections import ChainMap class HierarchicalChainMap: validate = lambda s, x: x namespaces = ( "backends", "lines", "patches", "hatches", "boxplot", "font", "text", "latex", "axes", "dates", "ticks", "grids", "legend", "figure", "images", "contour", "errorbar", "histogram", "scatter", "agg", "paths", "savefig", "keymaps", "animation", ) def __init__(self, *args, **kwargs): self._namespace_maps = {name: ChainMap({}) for name in self.namespaces} self.update(*args, **kwargs) self._namespace_maps = { name: mapping.new_child() for name, mapping in self._namespace_maps.items() } self._mapping = ChainMap(*self._namespace_maps.values()) def _split_key(self, key, sep="."): keys = key.split(sep, maxsplit=1) return keys, len(keys) def _get(self, key): keys, depth = self._split_key(key) if depth == 1: return self._namespace_maps[key] elif depth == 2: return self._namespace_maps[keys[0]].get(keys[1]) def __getitem__(self, key): self.validate(key) return self._get(key) def _set(self, key, value): keys, depth = self._split_key(key) if depth == 1: self._namespace_maps[key] = value elif depth == 2: self._namespace_maps[keys[0]][keys[1]] = value def __setitem__(self, key, value): self.validate(key) self._set(key, value) def __delitem__(self, key): keys, depth = self._split_key(key) if depth == 1: del self._namespace_maps[key] elif depth == 2: del self._namespace_maps[keys[0]][keys[1]] def __iter__(self): """Yield from sorted list of keys""" yield from sorted(self.keys()) def __len__(self): return sum(len(mapping) for mapping in self._namespace_maps) def __repr__(self): return repr(dict(rcParams.items())) def keys(self): keys = ( ".".join((space, key)) for space, mapping in self._namespace_maps.items() for key in mapping.keys() ) return keys def values(self): for key in self.keys(): yield self[key] def items(self): for key, value in zip(self.keys(), self.values()): yield key, value def pop(self, key): keys, depth = self._split_key(key) if depth == 1: self._mapping.pop() elif depth == 2: self._namespace_mapping[keys[0]].pop(keys[1]) def popitem(self): return self._mapping.popitem() def clear(self): self._mapping.clear() def setdefault(self, key, default=None): self._mapping[key] = default return default def get(self, key, default=None): return self._mapping[key] def update(self, other=(), /, **kwds): """D.update([E, ]**F) -> None. Update D from mapping/iterable E and F. If E present and has a .keys() method, does: for k in E: D[k] = E[k] If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v In either case, this is followed by: for k, v in F.items(): D[k] = v """ if isinstance(other, Mapping): for key in other: self[key] = other[key] elif hasattr(other, "keys"): for key in other.keys(): self[key] = other[key] else: for key, value in other: self[key] = value for key, value in kwds.items(): self[key] = value MutableMapping.register(HierarchicalChainMap)
__main__.HierarchicalChainMap
I can then use this class as a new data structure for rcParams
.
rcParams = HierarchicalChainMap({"backends.key1": "abc", "lines.key2": "xyz"})
rcParams
backends.key1 | : | abc | lines.key2 | : | xyz |
Next step is to try and implement a context manager.
import contextlib from copy import deepcopy @contextlib.contextmanager def rc_context(rc=None, fname=None): orig = deepcopy(rcParams) try: for space in rcParams._namespace_maps.keys(): rcParams._namespace_maps[space] = rcParams._namespace_maps[ space ].new_child() rcParams.update(rc) yield finally: for space in rcParams._namespace_maps.keys(): rcParams._namespace_maps[space] = rcParams._namespace_maps[ space ].parents
with rc_context(rc={"backends.key1": 123}): print("\nwithin context") print(rcParams) print("\nafter context") print(rcParams)
within context {'backends.key1': 123, 'lines.key2': 'xyz'} after context {'backends.key1': 'abc', 'lines.key2': 'xyz'}
The namespaces can also be accessed directly with their names
rcParams["backends"]
ChainMap({}, {'key1': 'abc'})