Source code for PyExpLabSys.settings

"""This module contains the modules used for settings for PyExpLabSys

To use the settings module instantiate a :class:`.Settings` object and access the
settings as attributes::

 >>> from PyExpLabSys.settings import Settings
 >>> settings = Settings()
 >>> settings.util_log_max_emails_per_period
 5

The settings in the :class:`.Settings` are formed by 2 layers. The bottom layer are
the defaults, that are stored in the
`PyExpLabSys/defaults.yaml
<https://github.com/CINF/PyExpLabSys/blob/master/PyExpLabSys/defaults.yaml>`_ file.
On top of those are placed the user settings, that originate from the file whose path
is in the `settings.USERSETTINGS_PATH` variable. The user settings can me modified
at run time as opposed to having to write them to the user settings file before
running. This is done simply by writing to the properties on the settings object::

 >>> settings.util_log_max_emails_per_period = 7
 >>> settings.util_log_max_emails_per_period
 7

All :class:`.Settings` objects share the same settings, so these changes will be
used when using other parts of PyExpLabSys that makes use of one of the settings. Do
however note, that different parts of PyExpLabSys use the settings at different times
(instantiate, call etc.) so check with the documentation for each component when the
settings needs to be modified to take effect.

"""

# Implementation status
# =====================
# The following files has had settings incoorporated into them OR evaluated not to
# need them:
# common/sockets.py (Done)
# common/utilities.py (Implemented but may need refactoring)


import sys
import os
import logging
from pathlib import Path
from pprint import pformat
from threading import Lock
from collections import ChainMap
import yaml


# Configure logging
LOG = logging.getLogger(__name__)
LOG.addHandler(logging.NullHandler())


# Form path for defaults and user settings files
# THISDIR = path.dirname(path.abspath(__file__))
# print(THISDIR)
# DEFAULT_PATH = path.join(THISDIR, 'defaults.yaml')
# DEFAULT_PATH = Path.cwd() / 'defaults.yaml'


# User settings
if os.environ.get('READTHEDOCS') == 'True':
    # Special case for read the docs
    USERSETTINGS_PATH = Path.cwd().parents[0] / 'bootstrap' / 'user_settings.yaml'
else:
    # Krabbe is the sole responsible person for the MAC check, if it breaks bug him
    if sys.platform.lower().startswith('linux') or sys.platform.lower() == "darwin":
        USERSETTINGS_PATH = (
            Path.home() / '.config' / 'PyExpLabSys' / 'user_settings.yaml'
        )
    else:
        USERSETTINGS_PATH = None


[docs]def value_str(obj): """Return a object and type str or NOT_SET if obj is None""" if obj is None: return 'NOT_SET' else: return '{} ({})'.format(obj, obj.__class__.__name__)
[docs]class Settings(object): """The PyExpLabSys settings object The settings are available to get and setable on this object as attributes i.e:: >>> from PyExpLabSys.settings import Settings >>> settings = Settings() >>> settings.util_log_max_emails_per_period 5 The settings are stored as a ChainMap of the defaults and the user settings and this ChainMap object containing the current state of the settings is shared between all :class:`.Settings` objects. To get a list of all available settings see the :attr:`.Settings.settings_names` attribute. To get a pretty print of all settings names, types, default values, user setting values (if any) use the :meth:`.Settings.print_settings` method. """ #: The settings ChainMap settings = None #: The available setting names settings_names = None # Access lock used to make sure the settings are consistent across threads _access_lock = Lock()
[docs] def __init__(self): LOG.info('Init') with self._access_lock: if self.settings is None: self._load_settings()
def _load_settings(self): """Load the defaults and user settings This is done when the first Settings object is instantiated """ default_settings = { 'sql_server_host': None, 'sql_database': None, 'common_sql_reader_user': None, 'common_sql_reader_password': None, 'common_liveserver_host': None, 'common_liveserver_port': None, 'util_log_warning_email': None, 'util_log_error_email': None, 'util_log_mail_host': None, 'util_log_max_emails_per_period': 5, 'util_log_email_throttle_time': 86400, # 1 day 'util_log_backlog_limit': 250, } user_settings = {} if USERSETTINGS_PATH is not None and USERSETTINGS_PATH.exists(): try: user_settings = yaml.load( USERSETTINGS_PATH.read_text(), yaml.SafeLoader ) except Exception: LOG.exception('Exception during loading of user settings') # FIXME check user_settings keys else: msg = 'No user settings found, file %s does not exist or is not readable' LOG.info(msg, USERSETTINGS_PATH) self.__class__.settings = ChainMap(user_settings, default_settings) self.__class__.settings_names = list(self.settings.keys()) def __setattr__(self, key, value): """Set attribute""" with self._access_lock: if key in self.settings: self.settings[key] = value else: msg = 'Only settings that have a default can be set. They are:\n{}' # Pretty format the list of names in the exception raise AttributeError(msg.format(pformat(self.settings_names))) def __getattr__(self, key): """Get attribute""" with self._access_lock: if key in self.settings: value = self.settings[key] else: msg = 'Invalid settings name: {}. Available settings are:\n{}' # Pretty format the list of names in the exception raise AttributeError(msg.format(key, pformat(self.settings_names))) if value is None: msg = ( 'The setting "{}" is indicated in the defaults as *requiring* a ' 'user setting before it can be used. Fill in the value in the ' 'user settings file "{}" or instantiate a ' 'PyExpLabSys.settings.Settings object and set the value there, ' '*before* attempting to use it.' ) raise AttributeError(msg.format(key, USERSETTINGS_PATH)) return value
[docs] def print_settings(self): """Pretty print of all default and user settings""" user_settings, default_settings = self.settings.maps print_template = '{{: <{}}}: {{: <{}}} {{: <{}}}' # Calculate key length max_key_length = max(len(str(key)) for key in self.settings) # Form default values strings (w. type) and calculate max length default_strs = {k: value_str(v) for k, v in default_settings.items()} # The 7 is the length of NOT_SET and Default max_default_value_length = max(7, *[len(v) for v in default_strs.values()]) # Form settings value strings (w. type) and calculate max length user_strs = {k: value_str(v) for k, v in user_settings.items()} # The 4 is the length of User max_settings_value_length = max(4, *[len(v) for v in user_strs.values()]) # Format max lengths into print_template print_template = print_template.format( max_key_length, max_default_value_length, max_settings_value_length ) # Printout the settings print('Settings') print(print_template.format('Key', 'Default', 'User')) print('=' * len(print_template.format('', '', ''))) for key in sorted(self.settings.keys()): default = default_strs[key] print(print_template.format(key, default, user_strs.get(key, '')))
[docs]def main(): """Main function used to simple testing""" # logging.basicConfig(level=logging.DEBUG) settings = Settings() print(settings.util_log_backlog_limit) settings.util_log_backlog_limit = 8 print(settings.util_log_backlog_limit) print(settings.util_log_warning_email) print() settings.print_settings()
if __name__ == '__main__': main()