Source code for recipe_system.config

#
#                                                                        DRAGONS
#
#                                                                  recipe_system
#                                                                      config.py
# ------------------------------------------------------------------------------
# CONFIG SERVICE

"""
This module provides an interface to config files, and a globally available
config object, to share setup information across the application.

An instance of `ConfigObject`, `globalConf`, is initialized when first loading
this module, and it should be used as the only interface to the config system.

"""
import os
from collections import defaultdict
from configparser import ConfigParser

DEFAULT_DIRECTORY = '~/.geminidr'
STANDARD_REDUCTION_CONF = '~/.geminidr/rsys.cfg'


[docs]class ConfigObject: """ Generalized wrapper on ConfigParser. This class is used for reading, parsing, and updating configurations for DRAGONS calibration service. Attributes ---------- _sections : <dict> _conv : <dict> A "converter" dictionary providing a key pointing to a native python data type. E.g., {(section, option): bool}. In the case of the DRAGONS calibration service, the boolean 'standalone' configuration option, this dictionary looks like, {('calibs', 'standalone'): bool} _exports : <dict> """ def __init__(self): self._sections = {} self._conv = {} self._exports = defaultdict(set) def __getitem__(self, item): try: return self._sections[item.lower()] except KeyError: raise KeyError("There is no {!r} section".format(item))
[docs] def export_section(self, section): """ Some options from the specified section may be published as environment variables, where spawned processes can find them. The exported variables would be the ones speficied using `update_exports`. Parameters ---------- section : <str> Section name, e.g., 'calibs' """ try: sect = self._sections[section] except KeyError: # Nothing to export... return for option in self._exports.get(section, ()): try: env = environment_variable_name(section, option) os.environ[env] = str(getattr(sect, option)) except AttributeError: # The option was not defined... pass
[docs] def load(self, filenames, defaults=None, env_override=False): """ Loads all or some entries from the specified section in a config file. The extracted values are set as environment variables, so that they are available at a later point to other modules or spawned processes. Parameters ---------- filenames : <str> or <iterable> A string or a sequence of strings containing the path(s) to configuration file(s). If a value is present in more than one file, the latest one to be processed overrides the preceding ones. Paths can start with `~/`, meaning the user home directory. defaults : <dict>, optional If some options are not found, and you want to set up a default value, specify them in here. Every key in the dictionary is the name of a section in the config file, and each element is another dictionary establishing attribute-value pairs for that section. env_override : <bool>, optional (default is False) If true, after loading values from the configuration files, the environment will be explored in search of options passed down by the parent process. Those options will override the ones taken from the config files. """ if isinstance(filenames, str): filenames = (filenames,) # Set the default values if defaults is not None: for section, sub_items in list(defaults.items()): current_section_conf = self._sections.get(section, Section({})).as_dict() for key, value in list(sub_items.items()): if key not in current_section_conf: current_section_conf[key] = value self._sections[section] = Section(current_section_conf) cp = ConfigParser() cp.read(list(map(os.path.expanduser, filenames))) translate = Converter(self._conv.copy(), cp) # Coerce values and apply overrides for section in cp.sections(): values = {} for key in cp.options(section): try: values[key] = os.path.expanduser( translate.from_config_file(section, key)) except TypeError: values[key] = translate.from_config_file(section, key) except AttributeError: values[key] = translate.from_config_file(section, key) if env_override: for key in values: env = environment_variable_name(section, key) if env in os.environ: values[key] = translate.from_raw(section, key, os.environ[env]) self.update(section, values)
[docs] def update(self, section, values): """ Regenerates a section from scratch. If the section had been loaded before, it will take the previous values as a basis and update them with the new ones. Parameters ---------- section: <str> The configuration section to update. values: <dict> The values associated to the section. """ prev = self._sections[section].as_dict() if section in self._sections else {} prev.update(values) self._sections[section] = Section(prev)
[docs] def update_exports(self, expdict): """ Updates the internal export table that will be used to share config information with process spawns. Parameters ---------- expdict : <dict> Each key is the name of a section. The values of the dictionary are sequences of strings, with each string in the sequence being the name of a config entry in that section that will be exported, if found. """ for section, opts in list(expdict.items()): self._exports[section].update(opts)
[docs] def update_translation(self, conv): """ Updates the internal mapping table for automatic translation of data types when reading from config files. Parameters ---------- conv : <dict> A mapping `(section_name, item)` -> Python type. Used internally for type translation when reading values from the config file. If a section/item pair is missing then a fallback `(None, item)` will be tried. If no match is found, no translation will be performed. The only types to be considered are: `int`, `float`, `bool` """ self._conv.update(conv)
[docs]class Converter: def __init__(self, conv_dict, cp): """ The class provides an internal mapping table for automatic translation to native data types when reading from config files. Parameters ---------- conv : <dict> A mapping `(section_name, item)` -> Python type. Used internally for type translation when reading values from the config file. If a section/item pair is missing then a fallback `(None, item)` will be tried. If no match is found, no translation will be performed. Mapped types: `int`, `float`, `bool` E.g., {('calibs', 'standalone'): bool} cp: <obj> An instance of ConfigParser. """ self._trans = dict(conv_dict) self._cp_default = cp.get self._type_to_cp = { bool: cp.getboolean, float: cp.getfloat, int: cp.getint }
[docs] def from_config_file(self, section, key): try: return self._type_to_cp[self._trans[(section, key)]](section, key) except KeyError: return self._type_to_cp.get((None, key), self._cp_default)(section, key)
[docs] def from_raw(self, section, key, value): return self._trans.get(key, str)(value)
[docs]class Section: """ An instance of `Section` describes the contents for a section of an INI-style config file. Each entry in the section translates to an *attribute* of the instance. Thus, a piece of config file like this:: [section] attribute1 = true attribute2 = /foo/bar could be accessed like this:: >>> sect = globalConf[SECTION_NAME] >>> sect.attribute1 'true' >>> sect.attribute2 '/foo/bar' The attributes are read-only. Any attempt to set a new one, or change the value of an entry through instances of this class, will raise an exception. As the entries will be translated as Python attributes, this means that entry names **have to** be valid Python identifier names. There is only one reserved name: `as_dict`. This cannot be used as an entry name. """ def __init__(self, values_dict): self._set('_contents', values_dict) def _set(self, name, value): if name == 'as_dict': raise RuntimeError("'as_dict' is a reserved name and cannot be " "used as a config entry.") self.__dict__[name] = value
[docs] def as_dict(self): "Returns a dictionary representation of this section" return self._contents.copy()
def __setattr__(self, attr, value): raise RuntimeError("Attribute {!r} is read-only".format(attr)) def __getattr__(self, attr): try: return self._contents[attr] except KeyError: raise AttributeError("Unknown attribute {!r}".format(attr)) def __repr__(self): return "<Section [{}]>".format(', '.join(list(self._contents.keys())))
[docs]def environment_variable_name(section, option): return '_GEM_{}_{}'.format(section.upper(), option.upper())
globalConf = ConfigObject()