Source code for frequent.config

# -*- coding: utf-8 -*-
#
#   This module is part of the Frequent project, Copyright (C) 2019,
#   Douglas Daly.  The Frequent package is free software, licensed under
#   the MIT License.
#
#   Source Code:
#       https://github.com/douglasdaly/frequent-py
#   Documentation:
#       https://frequent-py.readthedocs.io/en/latest
#   License:
#       https://frequent-py.readthedocs.io/en/latest/license.html
#
"""
Configuration module for global configuration settings.
"""
from collections.abc import MutableMapping
from contextlib import contextmanager
from copy import copy
from functools import wraps
import json
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterator as T_Iterator
from typing import Optional
from typing import Tuple
from typing import Type

__all__ = [
    'Configuration',
    'clear_config',
    'get_config',
    'load_config',
    'save_config',
    'set_config',
    'temp_config',
]


_GLOBAL_CONFIG: Optional['Configuration'] = None


def _make_sentinel(name: str = '_MISSING') -> 'Sentinel':
    """Creates a new sentinel object, code adapted from boltons:
        https://github.com/mahmoud/boltons/
    """
    class Sentinel(object):
        def __init__(self):
            self.name = name

        def __repr__(self):
            return '%s(%r)' % (self.__class__.__name__, self.name)

        def __nonzero__(self):
            return False

        __bool__ = __nonzero__

    return Sentinel()


_MISSING = _make_sentinel()


[docs]class Configuration(MutableMapping): """ Configuration storage object. This object is basically a :obj:`dict` with some additional bells and whistles, including: - The ability to access/modify items like attributes. - Serialize to/from strings via `dumps` and `loads`. - `save` and `load` to/from files. - Easily convert standard :obj:`dict` objects with `to_dict` and `from_dict`. Examples -------- This object works like a :obj:`dict`, where settings can be retrieved and set using: >>> config = Configuration() >>> config['answer'] = 42 >>> config['answer'] 42 Additionally, you can nest settings using the `.` as a seperator, for instance: >>> config['nested.setting'] = 'value' >>> config['nested'] {'setting': 'value'} >>> config['nested.setting'] 'value' >>> config['nested']['setting'] 'value' Furthermore, you can work with settings as if they were attributes: >>> config.nested.setting 'value' >>> config.dirs.temp = '/home/doug/tmp' >>> config['dirs.temp'] '/home/doug/tmp' """ __key_seperator__ = '.' def __init__(self, *args, **kwargs) -> None: self._storage = dict() return super().__init__(*args, **kwargs) def __repr__(self) -> str: return f"<{self.__class__.__name__} settings={repr(self._storage)}>" def __getitem__(self, key: str) -> Any: key, subkey = self._key_helper(key) rv = self._storage[key] if subkey: rv = rv[subkey] return rv def __setitem__(self, key: str, value: Any) -> None: key, subkey = self._key_helper(key) if isinstance(value, dict): value = self.__class__(value) if subkey: if key not in self._storage: self._storage[key] = self.__class__() self._storage[key][subkey] = value else: self._storage[key] = value return def __delitem__(self, key: str) -> None: key, subkey = self._key_helper(key) if subkey: del self._storage[key][subkey] else: del self._storage[key] return def __len__(self) -> int: return len(self._storage) def __iter__(self) -> T_Iterator: return iter(self._storage) def __getattr__(self, name: str) -> Any: try: rv = super().__getattribute__(name) except AttributeError as ex: try: rv = self[name] except KeyError: raise ex return rv def __setattr__(self, name: str, value: Any) -> None: if name != '_storage' and name not in self.__dict__: return self.__setitem__(name, value) return super().__setattr__(name, value) def __delattr__(self, name: str) -> None: if name == '_storage': raise Exception('Cannot delete _storage object') elif name not in self.__dict__: return self.__delitem__(name) return super().__delattr__(name)
[docs] def clear(self) -> None: """Clears all the settings stored in this configuration.""" return self._storage.clear()
[docs] def copy(self) -> 'Configuration': """Creates a copy of this configuration object. Returns ------- Configuration A copy of this configuration object. """ rv = self.__class__() rv.update(copy(self._storage)) return rv
[docs] def dumps(self, compact: bool = True, **kwargs) -> str: """Serializes this configuration object to a string. The default method uses the built-in python json library to convert this configuration to a JSON string. To use another method or format override this method. Parameters ---------- compact : bool, optional Make the returned representation as compact as possible (default is :obj:`True`). Returns ------- str String-serialized representation of this configuration. """ json_kws = {} json_kws['sort_keys'] = True if not compact: json_kws['indent'] = 2 json_kws.update(kwargs) return json.dumps(self.to_dict(), **json_kws)
[docs] @classmethod def loads(cls, text: str, **kwargs) -> 'Configuration': """Creates a new configuration from the given string data. Parameters ---------- text : str String-serialized representation to create the new object from. Returns ------- Configuration The newly created configuration from the given string data. """ data = json.loads(text, **kwargs) return cls.from_dict(data)
[docs] def save(self, path: str, **kwargs) -> None: """Saves this configuration object to the file path specified. Parameters ---------- path : str File path to save the configuration object to. kwargs : optional Additional parameters to pass through to the `dumps` call. See Also -------- dumps """ with open(path, 'w') as fout: fout.write(self.dumps(compact=False, **kwargs)) return
[docs] @classmethod def load(cls, path: str, **kwargs) -> 'Configuration': """Loads a configuration object from the file path specified. Parameters ---------- path : str File path to load the configuration object from. kwargs : optional Additional parameters to pass through to the `loads` call. Returns ------- Configuration The :obj:`Configuration` object loaded from the `path` given. See Also -------- loads """ with open(path, 'r') as fin: text = fin.readlines() return cls.loads(''.join(text), **kwargs)
[docs] @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Configuration': """Creates a configuration object from the given :obj:`dict`. Parameters ---------- data : dict Dictionary to generate the new configuration object with. Returns ------- Configuration The new configuration from the given dictionary data. """ rv = cls() for k, v in data.items(): if isinstance(v, dict): v = cls.from_dict(v) rv[k] = v return rv
[docs] def to_dict(self) -> Dict[str, Any]: """Converts this configuration object to a standard :obj:`dict`. Returns ------- dict Standard :obj:`dict` version of this configuration object. """ rv = {} for k, v in self.items(): if isinstance(v, Configuration): v = v.to_dict() rv[k] = v return rv
@classmethod def _key_helper(cls, key: str) -> Tuple[str, str]: """Splits the given key into a key & subkey""" if cls.__key_seperator__ in key: r_key, r_subkey = key.split(cls.__key_seperator__, 1) else: r_key = key r_subkey = None return r_key, r_subkey
[docs]def load_config( path: str = None, config_cls: Type[Configuration] = Configuration ) -> None: """Loads the global configuration from the given file path. Parameters ---------- path : str, optional The file path to load the configuration file from. If this is not provided a new, empty :obj:`Configuration` is loaded. config_cls : type, optional The type of :obj:`Configuration` to load (the default is the standard :obj:`Configuration` class). """ global _GLOBAL_CONFIG if path: _GLOBAL_CONFIG = config_cls.load(path) else: _GLOBAL_CONFIG = config_cls() return
def _ensure_config(f: Callable) -> Callable: @wraps(f) def wrapper(*args, **kwargs): global _GLOBAL_CONFIG if _GLOBAL_CONFIG is None: load_config() return f(*args, **kwargs) return wrapper
[docs]@_ensure_config def save_config(path: str) -> None: """Saves the current global configuration to the given file path. Parameters ---------- path : str The file path to save the configuration to. """ global _GLOBAL_CONFIG return _GLOBAL_CONFIG.save(path)
[docs]@_ensure_config def get_config(name: str = None, default: Any = _MISSING) -> Any: """Gets the global configuration. Parameters ---------- name : str, optional The name of the setting to get the value for. If no name is given then the whole :obj:`Configuration` object is returned. default : optional The default value to return if `name` is provided but the setting doesn't exist in the global configuration. Returns ------- :obj:`Configuration` or :obj:`object` The global configuration object or the configuration setting requested. """ global _GLOBAL_CONFIG if not name: return _GLOBAL_CONFIG.copy() if default == _MISSING: return _GLOBAL_CONFIG[name] return _GLOBAL_CONFIG.get(name, default)
[docs]@_ensure_config def set_config(name: str, value: Any) -> None: """Sets a configuration setting. Parameters ---------- name : str The setting to set the `value` for. value : object The value to set for the given `name`. """ global _GLOBAL_CONFIG _GLOBAL_CONFIG[name] = value return
[docs]def clear_config() -> None: """Clears the currently-set global configuration.""" global _GLOBAL_CONFIG if _GLOBAL_CONFIG is not None: _GLOBAL_CONFIG.clear() _GLOBAL_CONFIG = None return
[docs]@contextmanager @_ensure_config def temp_config(**settings) -> Configuration: """Gets a context with a temporary configuration. Any changes made to the configuration via calls to :obj:`set_config` (or otherwise) will be made and persisted only within the context. The original configuration will be restored upon leaving the context. Parameters ---------- settings : optional Any temporary settings to set in the temporary configuration context. Yields ------ Configuration The temporary configuration object. """ global _GLOBAL_CONFIG curr_config = _GLOBAL_CONFIG.copy() try: for k, v in settings.items(): set_config(k, v) yield _GLOBAL_CONFIG.copy() finally: _GLOBAL_CONFIG = curr_config return