PKoMe$$confjson/__init__.py"""A bafflingly simple, JSON-backend configuration manager for python programs.""" __version__ = "1.3.0" import copy import json import pathlib DEFAULT_CONFIG_FILENAME = "default.config.json" USER_CONFIG_FILENAME = "user.config.json" class _ConfigItemProxy: """Proxy object for attribute-style access to config items.""" def __init__( self, dict_, use_placeholders=False, placeholder_parent=None, placeholder_key=None, ): super().__setattr__("_dict", dict_) super().__setattr__("_use_placeholders", use_placeholders) super().__setattr__("_placeholder_parent", placeholder_parent) super().__setattr__("_placeholder_key", placeholder_key) def __bool__(self): return bool(self._dict) def __contains__(self, key): return key in self._dict.keys() def __eq__(self, other): if self._dict is None: return False if isinstance(other, dict): return self._dict == other if isinstance(other, _ConfigItemProxy): return self._dict == other.get_dict() return False def __getattr__(self, key): if self._dict is None: return _ConfigItemProxy(None, True, self, key) try: value = self._dict[key] if isinstance(value, dict): return _ConfigItemProxy(value, self._use_placeholders) return value except KeyError: if self._use_placeholders: return _ConfigItemProxy(None, True, self, key) raise def __getitem__(self, key): if self._dict is None: return _ConfigItemProxy(None, True, self, key) try: value = self._dict[key] if isinstance(value, dict): return _ConfigItemProxy(value, self._use_placeholders) return value except KeyError: if self._use_placeholders: return _ConfigItemProxy(None, True, self, key) raise def __setattr__(self, key, value): json.dumps({key: value}) if self._dict is None: super().__setattr__("_dict", {}) self._placeholder_parent[self._placeholder_key] = self._dict super().__setattr__("_placeholder_parent", None) super().__setattr__("_placeholder_key", None) self._dict[key] = value def __setitem__(self, key, value): json.dumps({key: value}) if self._dict is None: super().__setattr__("_dict", {}) self._placeholder_parent[self._placeholder_key] = self._dict super().__setattr__("_placeholder_parent", None) super().__setattr__("_placeholder_key", None) self._dict[key] = value def get(self, key, default=None): """Return the value corresponding to `key` if it exists, else `default`.""" if key in self: return self[key] return default def get_dict(self): """Return the backing dict.""" return self._dict def items(self): """Return tuples consisting of every key-value pair in the ConfigItemProxy.""" return ((key, self[key]) for key in self.keys()) def keys(self): """Return every key in the ConfigItemProxy.""" return self._dict.keys() @property def is_placeholder(self): """Return True if object is a placeholder for a nonexistent dict item.""" return self._dict is None class Config: """A manager for JSON-backed default and user-specified config settings.""" def __init__( self, path, *, user_config_filename=USER_CONFIG_FILENAME, default_config_filename=DEFAULT_CONFIG_FILENAME, use_placeholders=False, ): pathlib_path = pathlib.Path(path) if pathlib_path.is_dir(): super().__setattr__("directory", pathlib_path) elif not pathlib_path.exists(): raise ValueError( "Parameter `path` must be the path of an existing file" f" or directory; '{path}' does not exist." ) else: super().__setattr__("directory", pathlib_path.parent) super().__setattr__( "default_config_path", self.directory / default_config_filename ) super().__setattr__("user_config_path", self.directory / user_config_filename) super().__setattr__("_default_dict", {}) super().__setattr__("_user_dict", {}) super().__setattr__("_original_attrs", dir(self)) super().__setattr__("_use_placeholders", use_placeholders) self.load() def __contains__(self, key): return key in self._user_dict or key in self._default_dict def __delitem__(self, key): del self._user_dict[key] def __getattr__(self, key): return self[key] def __getitem__(self, key): try: if key not in self._user_dict: self._user_dict[key] = copy.deepcopy(self._default_dict[key]) value = self._user_dict[key] if isinstance(value, dict): return _ConfigItemProxy(value, self._use_placeholders) return value except KeyError: if self._use_placeholders: return _ConfigItemProxy(None, True, self, key) raise def __len__(self): return len(self.keys()) def __setattr__(self, key, value): if key in self._original_attrs: raise KeyError( "Cannot use attribute-style access to set config item '{key}';" " attribute name is reserved." ) self[key] = value def __setitem__(self, key, value): # Will fail for values unsupported by JSON. json.dumps({key: value}) self._user_dict[key] = value def default_keys(self): """Get only the keys present in the default config.""" return self._default_dict.keys() def get(self, key, default=None): """Get the value of the given key from the user config, the default config or the optional `default` argument, in order of preference. """ try: return self[key] except KeyError: return default def get_default(self, key): """Get the default value of the given setting, even if there is a user setting. """ return self._default_dict[key] def keys(self): """Get the keys present in the config.""" return list(set(self._user_dict.keys()).union(self._default_dict.keys())) def load(self): """Load or reload config settings from the backing JSON files. Note that this will reset any unsaved user config settings. """ try: with self.default_config_path.open() as file: super().__setattr__("_default_dict", json.load(file)) except FileNotFoundError: super().__setattr__("_default_dict", {}) try: with self.user_config_path.open() as file: super().__setattr__( "_user_dict", _get_dict_union(json.load(file), self._default_dict) ) except FileNotFoundError: super().__setattr__("_user_dict", {}) def save(self): """Save any user config settings that differ from their respective default values. """ diff = _get_dict_diff(self._user_dict, self._default_dict) if diff: with self.user_config_path.open(mode="w") as file: json.dump(diff, file, indent=4, sort_keys=True) elif self.user_config_path.exists(): self.user_config_path.unlink() def _get_dict_diff(top_dict, bottom_dict): result_dict = {} for key, top_value in top_dict.items(): if key in bottom_dict: bottom_value = bottom_dict[key] if top_value != bottom_value: if isinstance(top_value, dict) and isinstance(bottom_value, dict): result_dict[key] = _get_dict_diff(top_value, bottom_value) else: result_dict[key] = top_value else: result_dict[key] = top_value return result_dict def _get_dict_union(top_dict, bottom_dict): result_dict = {} for key in set(top_dict.keys()).union(bottom_dict.keys()): if key in top_dict: top_value = top_dict[key] if ( isinstance(top_value, dict) and key in bottom_dict and isinstance(bottom_dict[key], dict) ): result_dict[key] = _get_dict_union(top_value, bottom_dict[key]) else: result_dict[key] = copy.deepcopy(top_value) else: result_dict[key] = copy.deepcopy(bottom_dict[key]) return result_dict PKΡGM 7 confjson-1.3.0.dist-info/LICENSEThis is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to PK!H>*RQconfjson-1.3.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,rzd&Y)r$[)T&UrPK!H Ɏ!confjson-1.3.0.dist-info/METADATAWn}|E~HFwemو 3͙^t3=&A=]֋|!RuTթUK`"URl߻ȿͿU-r[5=Ӯ}Fx;Ybm)&LyqZeU=-uk,jCݵzaz~^XwpyXuv*a7ϷU~JoJEWVaXVfJVvzS3^'5\~){C͚O}Ϩ9hXnkF`F +uIW]i-4MNot O){e?1{Ϗn 4ɡnј90G@C`Q|b)1^*>3MIwQ.|9٥,z1aRꥬaS}eʏq5\H&eQ_OO+0wohp6[xU5\2^=_Cp%]H$72TvǨ@͢yن7*f^-I%R\)b "o .i$c'VGLiȶt_Ty2y8z2B Ҁ#<(0k{Gȕ;joQ.G D4P^ ZB v_'<胈]J)tϺӗXMwD&@6M)}ੱv>sƓ䅩XZaJ`󸚭U5%UOCǾ*W㏺{pP횧P"YOl)eEyQDEG8rBWH4::$_D Yky~Ix*_ƤC2HSKXKo^x~Fsщ@Xw0+ }Rz+3:)T2y:t@7-m]{OB웝Z 4#Jj=>2UpWl Rr%9+ęFEUԺX-35X`4)ߛGY$L2.,nVKja',Ejoc7abpƒs#`TsPK!HqTwconfjson-1.3.0.dist-info/RECORDu̱r0o ! R 䔞N*1ߥxWrʹBsnwo_pdfM)A,/ӏ~g5E16T5!&fg;%YMxA{2)\8uٕ7c ݂A[1zh<.IvY<ߖ*āyC$k'݋&0w a8  '~F4WX'fVywlg9]G,<`PKoMe$$confjson/__init__.pyPKΡGM 7 9$confjson-1.3.0.dist-info/LICENSEPK!H>*RQI)confjson-1.3.0.dist-info/WHEELPK!H Ɏ!)confjson-1.3.0.dist-info/METADATAPK!HqTw0confjson-1.3.0.dist-info/RECORDPKx62