Source code for labtest.configobj

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from future import standard_library
from future.utils import iteritems
from builtins import object
import click
standard_library.install_aliases()


[docs]class Config(object): # A mapping of extensions to method names to use when processing config files extension_map = { '.cfg': 'read_ini_config', '.ini': 'read_ini_config', '.json': 'read_json_config', '.yml': 'read_yaml_config', '.yaml': 'read_yaml_config', } # Where to automatically look for the configuration information default_config_files = [ '.labtest.yml', 'setup.cfg', 'package.json', ] # namespace is the section under which it looks for the key/values # in the config file namespace = 'config' # required_attrs is are the configuration attributes required before continuing # It is used by the validation method required_attrs = [] def __init__(self, **kwargs): self._config = {} self._validation_errors = {} for key, val in iteritems(kwargs): setattr(self, key, val) @property def config(self): """ This returns the configuration as a dict. This method will use the logic in `__getattr__` to set the values. It doesn't just return the default `_config` dict. """ cfg = {} for item in self.required_attrs: if item not in self._config: default_func = getattr(self, 'get_default_{}'.format(item), None) if default_func: cfg[item] = default_func() for key, val in iteritems(self._config): cfg[key] = getattr(self, key) return cfg
[docs] def validate_dependencies(self): """ Subclasses should override this command to provide validation for options that are required based on other option's values """ pass
[docs] def validate(self): """ Validate the configuration. Set the attribute `validation_errors` """ self._validation_errors = {} is_valid = True config = self.config missing_attrs = [] for attr in self.required_attrs: if attr not in config: is_valid = False missing_attrs.append(attr) missing_attrs.extend(self.validate_dependencies()) if missing_attrs: self._validation_errors['Missing Attributes'] = missing_attrs return is_valid
[docs] def validation_message(self): """ Convenience method to format the validation errors, if any """ msg = [] if self._validation_errors: if 'Missing Attributes' in self._validation_errors: msg.append(click.style('The configuration is missing the following required attributes:', fg='red')) msg.append(', '.join(self._validation_errors['Missing Attributes'])) return ' '.join(msg) else: return click.style('The configuration is valid.', fg='green')
def __getattr__(self, name): """ Get a configuration attribute via several methods 1. Look for a `get_<attribute>` function and call it. This allows for some processing of the value if it needs to be stored one way, but used in another, or is a composite value. 2. Look for the attribute in the `_config` dict. The `_config` attribute is the local storage of the configuration attributes. 3. Look for a `get_default_<attribute>` function and call it. The value returned is set in the `_config` dict for next time before getting returned. 4. Raise `AttributeError` if nothing is found """ attr_name = 'get_{}'.format(name) attr_default_name = 'get_default_{}'.format(name) if name.startswith('get_'): raise AttributeError elif hasattr(self, attr_name): return getattr(self, attr_name)() elif name in self._config: return self._config[name] elif hasattr(self, attr_default_name): self._config[name] = getattr(self, attr_default_name)() return self._config[name] raise AttributeError('The configuration attribute "{}" is not set.'.format(name)) def __setattr__(self, name, value): """ Set the configuration attribute via a setter or directly in the `_config` 1. Attributes starting with `_` are automatically set. 2. If a `set_<attribute>` method exists, call it. This allows for processing of the value and validation. This function *must* update the attribute in `self._config` 3. Set the attribute in `_config` to the value passed. """ attr_name = 'set_{}'.format(name) if name.startswith('_'): object.__setattr__(self, name, value) elif hasattr(self, attr_name): fn = getattr(self, attr_name) fn(value) else: self._config[name] = value
[docs] def parse_default_config(self): """ Look for the default config path from the `default_config_files`. """ import os from dotenv import find_dotenv for option in self.default_config_files: path = find_dotenv(option, usecwd=True) if path: self.config_file_path = os.path.normpath(path) self.parse_file(path) break
[docs] def parse_file(self, filepath): """ Determine which method to use to parse the file, based on the file extension """ import os name, ext = os.path.splitext(filepath) if ext in self.extension_map: getattr(self, self.extension_map[ext])(filepath)
[docs] def read_ini_config(self, filepath): """ Read a configuration from an INI file """ import os from ConfigParser import ConfigParser if not os.path.exists(filepath): raise IOError() cfgparser = ConfigParser() cfgparser.read([filepath]) if self.namespace in cfgparser.sections(): for key, val in cfgparser.items(self.namespace): setattr(self, key, val)
[docs] def read_json_config(self, filepath): """ Read a configuration from a JSON file """ import json with open(filepath, 'r') as f: config = json.loads(f.read()) for key, val in iteritems(config.get(self.namespace, {})): setattr(self, key, val)
[docs] def read_yaml_config(self, filepath): """ Reads a configuration from a YAML file """ import oyaml as yaml with open(filepath, 'r') as f: config = yaml.load(f) for key, val in iteritems(config.get(self.namespace, {})): setattr(self, key, val)