#! /usr/bin/env python3 # encoding: utf-8 import jsmin import json class Config: """ A hierarchical key-value container that can be loaded from JSON or a Python dict. Any contained dicts are automatically converted to Config instances as well. Supports sequential overwriting - so one can load a default config and then progressively overwrite it with overlays: >>> raw {'a': 1, 'b': {'ba': 2, 'bb': 3, 'bc': {'bca': None}}, 'c': 4} >>> overlay {'b': {'bc': {'bca': 42}}} while stack: current = stack[-1] ... stack.pop() for key in overlay.keys(): """ def __init__(self, d, readonly=False): assert type(d) == dict object.__setattr__(self, "raw", d) object.__setattr__(self, "readonly", readonly) @classmethod def from_file(cls, path, readonly=False): with open(path) as f: initial = json.loads(jsmin.jsmin(f.read())) return cls(initial, readonly) def update(self, d): def keys(self): return self.raw.keys() def values(self): return self.raw.values() def items(self): return self.raw.items() def __getitem__(self, name): return self.raw[name] def __setitem__(self, name, value): if self.readonly: raise AttributeError("config is read-only") self.raw[name] = value def __setattr__(self, name, value): if self.readonly: raise AttributeError("config is read-only") self.raw[name] = value def __getattr__(self, name): matches = [key for key in self.raw if key.replace("-", "_") == name] if matches: assert len(matches) == 1 value = self.raw[matches[0]] if type(value) == dict: return Config(value) else: return value else: raise KeyError(name) def __contains__(self, name): return name in self.raw def __repr__(self): return "Config(%s)" %(" ".join("%s=%s" %(k, "…" if type(v) == dict else v) for k, v in self.raw.items()))