Source code for scm.plams.core.settings

__all__ = ['Settings']

[docs]class Settings(dict): """Automatic multi-level dictionary. Subclass of built-in class :class:`dict`. The shortcut dot notation (``s.basis`` instead of ``s['basis']``) can be used for keys that: * are strings * don't contain whitespaces * begin with a letter or an underscore * don't both begin and end with two or more underscores. Iteration follows lexicographical order (via :func:`sorted` function) Methods for displaying content (:meth:`~object.__str__` and :meth:`~object.__repr__`) are overridden to recursively show nested instances in easy-readable format. Regular dictionaries (also multi-level ones) used as values (or passed to the constructor) are automatically transformed to |Settings| instances:: >>> s = Settings({'a': {1: 'a1', 2: 'a2'}, 'b': {1: 'b1', 2: 'b2'}}) >>> s.a[3] = {'x': {12: 'q', 34: 'w'}, 'y': 7} >>> print(s) a: 1: a1 2: a2 3: x: 12: q 34: w y: 7 b: 1: b1 2: b2 """ def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) for k,v in self.items(): if isinstance(v, dict): self[k] = Settings(v)
[docs] def copy(self): """Return a new instance that is a copy of this one. Nested |Settings| instances are copied recursively, not linked. In practice this method works as a shallow copy: all "proper values" (leaf nodes) in the returned copy point to the same objects as the original instance (unless they are immutable, like ``int`` or ``tuple``). However, nested |Settings| instances (internal nodes) are copied in a deep-copy fashion. In other words, copying a |Settings| instance creates a brand new "tree skeleton" and populates its leaf nodes with values taken directly from the original instance. This behavior is illustrated by the following example:: >>> s = Settings() >>> s.a = 'string' >>> s.b = ['l','i','s','t'] >>> s.x.y = 12 >>> s.x.z = {'s','e','t'} >>> c = s.copy() >>> s.a += 'word' >>> s.b += [3] >>> s.x.u = 'new' >>> s.x.y += 10 >>> s.x.z.add(1) >>> print(c) a: string b: ['l', 'i', 's', 't', 3] x: y: 12 z: set([1, 's', 'e', 't']) >>> print(s) a: stringword b: ['l', 'i', 's', 't', 3] x: u: new y: 22 z: set([1, 's', 'e', 't']) This method is also used when :func:`python3:copy.copy` is called. """ ret = Settings() for name in self: if isinstance(self[name], Settings): ret[name] = self[name].copy() else: ret[name] = self[name] return ret
[docs] def soft_update(self, other): """Update this instance with data from *other*, but do not overwrite existing keys. Nested |Settings| instances are soft-updated recursively. In the following example ``s`` and ``o`` are previously prepared |Settings| instances:: >>> print(s) a: AA b: BB x: y1: XY1 y2: XY2 >>> print(o) a: O_AA c: O_CC x: y1: O_XY1 y3: O_XY3 >>> s.soft_update(o) >>> print(s) a: AA #original value s.a not overwritten by o.a b: BB c: O_CC x: y1: XY1 #original value s.x.y1 not overwritten by o.x.y1 y2: XY2 y3: O_XY3 *Other* can also be a regular dictionary. Of course in that case only top level keys are updated. Shortcut ``A += B`` can be used instead of ``A.soft_update(B)``. """ for name in other: if isinstance(other[name], Settings): if name not in self: self[name] = other[name].copy() elif isinstance(self[name], Settings): self[name].soft_update(other[name]) elif name not in self: self[name] = other[name] return self
[docs] def update(self, other): """Update this instance with data from *other*, overwriting existing keys. Nested |Settings| instances are updated recursively. In the following example ``s`` and ``o`` are previously prepared |Settings| instances:: >>> print(s) a: AA b: BB x: y1: XY1 y2: XY2 >>> print(o) a: O_AA c: O_CC x: y1: O_XY1 y3: O_XY3 >>> s.update(o) >>> print(s) a: O_AA #original value s.a overwritten by o.a b: BB c: O_CC x: y1: O_XY1 #original value s.x.y1 overwritten by o.x.y1 y2: XY2 y3: O_XY3 *Other* can also be a regular dictionary. Of course in that case only top level keys are updated. """ for name in other: if isinstance(other[name], Settings): if name not in self or not isinstance(self[name], Settings): self[name] = other[name].copy() else: self[name].update(other[name]) else: self[name] = other[name]
[docs] def merge(self, other): """Return new instance of |Settings| that is a copy of this instance soft-updated with *other*. Shortcut ``A + B`` can be used instead of ``A.merge(B)``. """ ret = self.copy() ret.soft_update(other) return ret
[docs] def find_case(self, key): """Check if this instance contains a key consisting of the same letters as *key*, but possibly with different case. If found, return such a key. If not, return *key*. When |Settings| are used in case-insensitive contexts, this helps preventing multiple occurences of the same key with different case:: >>> s = Settings() >>> s.system.key1 = value1 >>> s.System.key2 = value2 >>> print(s) System: key2: value2 system: key1: value1 >>> t = Settings() >>> t.system.key1 = value1 >>> t[t.find_case('System')].key2 = value2 >>> print(t) system: key1: value1 key2: value2 """ lowkey = key.lower() for k in self: if k.lower() == lowkey: return k return key
[docs] def as_dict(self): """Return a copy of this instance with all |Settings| replaced by regular Python dictionaries. """ d = {} for k, v in self.items(): if not isinstance(v, Settings): d[k] = v else: d[k] = v.as_dict() return d
#=======================================================================
[docs] def __iter__(self): """Iteration through keys follows lexicographical order.""" return iter(sorted(self.keys()))
[docs] def __missing__(self, name): """When requested key is not present, add it with an empty |Settings| instance as a value. This method is essential for automatic insertions in deeper levels. Without it things like:: >>> s = Settings() >>> s.a.b.c = 12 will not work. """ self[name] = Settings() return self[name]
[docs] def __setitem__(self, name, value): """Like regular ``__setitem__``, but if the value is a dict, convert it to |Settings|.""" if isinstance(value, dict): value = Settings(value) dict.__setitem__(self, name, value)
[docs] def __getattr__(self, name): """If name is not a magic method, redirect it to ``__getitem__``.""" if (name.startswith('__') and name.endswith('__')): return dict.__getattr__(self, name) return self[name]
[docs] def __setattr__(self, name, value): """If name is not a magic method, redirect it to ``__setitem__``.""" if name.startswith('__') and name.endswith('__'): dict.__setattr__(self, name, value) self[name] = value
[docs] def __delattr__(self, name): """If name is not a magic method, redirect it to ``__delitem__``.""" if name.startswith('__') and name.endswith('__'): dict.__delattr__(self, name) del self[name]
[docs] def _str(self, indent): """Print contents with *indent* spaces of indentation. Recursively used for printing nested |Settings| instances with proper indentation.""" ret = '' for name in self: value = self[name] ret += ' '*indent + str(name) + ': \t' if isinstance(value, Settings): ret += '\n' + value._str(indent+len(str(name))+1) else: ret += str(value) + '\n' return ret
def __str__(self): return self._str(0) __repr__ = __str__ __iadd__ = soft_update __add__ = merge __copy__ = copy