Source code for xtd.core.config.manager

# -*- coding: utf-8
# pylint: disable=unused-import,deprecated-module
#------------------------------------------------------------------#

__author__    = "Xavier MARCELET <xavier@marcelet.com>"

#------------------------------------------------------------------#

import re
import json
import optparse
import sys
from future.utils    import with_metaclass

from .formatter        import IndentedHelpFormatterWithNL
from ..error           import ConfigValueError, ConfigError
from ..                import mixin

#------------------------------------------------------------------#

[docs]class Option(object): """ Option object for :py:class:`ConfigManager` Available option properties: config Allow option to be read from configuration file, default ``True`` cmdline Allow option to be read from command line, default ``True`` default Internal default value for option, default ``None`` valued Expects a value for option. Default ``True`` if default value profived. For non-valued options, default value is ``False`` and reading them from command line will store a ``True`` value description Option description to display on usage message checks Array of functions to validate option value. You may provide a single function. Default ``[]``. See :py:mod:`xtd.core.config.checkers` for standard check functions. longopt Override long option name. Long options has be be unique. Default ``--<section>-<name>``. mandatory Option is mandatory on command line, often used with non-valued options. Default ``False`` Note: Provided check callback must respect the following signature : .. code-block:: python def function(p_section, p_section, p_value) They must return the input ``p_value`` (possible possibly trans-typed) and raise :py:exc:`~xtd.core.error.ConfigError` if value is rejected See :py:mod:`xtd.core.config.checkers` for standard check functions. Args: p_section (str): option's section name p_name (str): option's name p_props (dict) : option definition Raises: xtd.core.error.ConfigError: encountered unknown property """
[docs] def __init__(self, p_section, p_name, p_prop=None): self.m_section = p_section self.m_name = p_name self.m_config = True self.m_cmdline = True self.m_default = None self.m_valued = False self.m_description = "undocumented option" self.m_checks = [] self.m_longopt = "--%s-%s" % (p_section, p_name) self.m_mandatory = None if p_prop is not None: self._update(p_prop)
def _update(self, p_props): l_keys = [ x[2:] for x in dir(self) if x[0:2] == "m_" ] for c_key,c_val in p_props.items(): if not c_key in l_keys: raise ConfigError("invalid option property '%s'" % c_key) if c_key == "checks" and not isinstance(c_val, list): c_val = [ c_val ] setattr(self, "m_%s" % c_key, c_val) if self.m_default != None: self.m_valued = True if not self.m_valued: self.m_default = False
[docs] def validate(self, p_value): for c_check in self.m_checks: p_value = c_check(self.m_section, self.m_name, p_value) return p_value
[docs]class ConfigManager(with_metaclass(mixin.Singleton, object)): """Unified command-line & file config option manager The main user methods are : * :py:meth:`register_section` * :py:meth:`get` * :py:meth:`set_usage` Main documentation for option definition : :py:class:`Option` Attributes: __metaclass__ (:py:class:`xtd.core.mixin.Singleton`) : makes this object a singleton """
[docs] def __init__(self): self.m_data = {} self.m_options = [] self.m_sections = {} self.m_usage = "usage: %prog [options]" self.m_cmdParser = None self.m_cmdOpts = None self.m_cmdArgs = []
[docs] def register_section(self, p_section, p_title, p_options): """ Register a set of options to a given section See :py:class:`Option` for full documentation of option properties Args: p_section (str): section tag p_title (str): the section title in the command-line usage p_options (list of dict): options definition Returns: ConfigManager: self Raises: xtd.core.error.ConfigError: invalid option definition """ self.m_sections[p_section] = p_title for c_opt in p_options: if not "name" in c_opt: raise ConfigError("missing mandatory option property 'name'") self.register(p_section, c_opt["name"], c_opt) return self
[docs] def register(self, p_section, p_name, p_props): """ Register an option in a specific section See :py:class:`Option` for full documentation of option properties Args: p_name (str): option name p_section (str): section name p_props (dict): option properties Returns: ConfigManager: self """ l_option = Option(p_section, p_name, p_props) self.m_options.append(l_option) return self
[docs] def sections(self): """ Get sections tags Returns: (list): array of str of all section names """ return list(self.m_data.keys())
[docs] def section_exists(self, p_section): """ Indicates if specified section has been registered Args: p_section (str): section name Returns: bool : true is ``p_section`` is registered """ return p_section in self.m_data
[docs] def options(self, p_section): """ Get the list of all registered option names for specefic a section Args: p_section (str): section name Raises: xtd.core.error.ConfigError: ``p_section`` not registered Returns: list: array of str of option names """ if not p_section in self.m_data: raise ConfigError("section '%s' doesn't exist" % p_section) return list(self.m_data[p_section].keys())
[docs] def option_exists(self, p_section, p_name): """ Indicates if specified option has been registered in section Args: p_section (str): section name p_option (str): option name Returns: bool : true is ``p_section`` is registered and contains ``p_option`` """ if not p_section in self.m_data: return False return p_name in self.m_data[p_section].keys()
[docs] def get(self, p_section, p_name): """ Get option value Args: p_section (str): section name p_option (str): option name Raises: xtd.core.error.ConfigValueError: section/option not found Returns: (undefined): current option value """ if not p_section in self.m_data or not p_name in self.m_data[p_section]: raise ConfigValueError(p_section, p_name, "unknown configuration entry") return self.m_data[p_section][p_name]
[docs] def set(self, p_section, p_name, p_value): """set option value Warning: This method stores the input value immediately without validating it against option's checks. Args: p_section (str): section name p_option (str): option name Raises: xtd.core.error.ConfigValueError: section/option not found """ if not p_section in self.m_data or not p_name in self.m_data[p_section]: raise ConfigValueError(p_section, p_name, "unknown configuration entry") self.m_data[p_section][p_name] = p_value
[docs] def help(self, p_file=None): """ Display command line help message Args: p_file (file): output stream, defaults to sys.stdout """ self.m_cmdParser.print_help(p_file)
[docs] def initialize(self): """ Initializes object Usually called by :py:class:`~xtd.core.application.Application` object. """ self.m_cmdParser = None self.m_cmdOpts = None self.m_cmdArgs = [] self.m_data = {} self._load_data() self._cmd_parser_create()
[docs] def parse(self, p_argv=None): """ Parses command line and file options Usually called by :py:class:`~xtd.core.application.Application` object. Args: p_argv (list of str) : list of command line arguments """ if p_argv is None: p_argv = sys.argv self._cmd_parser_load(p_argv) self._file_parser_load()
[docs] def get_name(self): """Get parsed application name ``sys.argv[0]`` Returns: str: program's ``sys.argv[0]`` """ return self.m_cmdArgs[0]
[docs] def get_args(self): """Get command line post-parse remaining options Returns: list: unparsed command line options """ return self.m_cmdArgs[1:]
[docs] def set_usage(self, p_usage): """Set command line usage message See :py:class:`optparse.OptionParser` Args: p_usage (str): usage string """ self.m_usage = p_usage
def _get_option(self, p_section, p_name): l_values = [ x for x in self.m_options if x.m_section == p_section and x.m_name == p_name ] if not len(l_values): raise ConfigValueError(p_section, p_name, "unknown configuration entry") return l_values[0] def _load_data(self): for c_option in self.m_options: if not c_option.m_section in self.m_data: self.m_data[c_option.m_section] = {} self.m_data[c_option.m_section][c_option.m_name] = c_option.m_default @staticmethod def _cmd_attribute_name(p_section, p_option): return "parse_%(section)s_%(key)s" % { "section" : p_section, "key" : p_option.replace("-", "_") } def _cmd_parser_create(self): self.m_cmdParser = optparse.OptionParser(usage=self.m_usage, formatter=IndentedHelpFormatterWithNL()) l_sections = set([ x.m_section for x in self.m_options ]) for c_section in sorted(l_sections): l_sectionName = self.m_sections.get(c_section, "") l_group = optparse.OptionGroup(self.m_cmdParser, l_sectionName) l_options = [ x for x in self.m_options if x.m_section == c_section and x.m_cmdline ] for c_opt in l_options: l_args = [] l_kwds = { "help" : c_opt.m_description, "default" : None, "action" : "store", "dest" : self._cmd_attribute_name(c_section, c_opt.m_name) } if not c_opt.m_valued: l_kwds["action"] = "store_true" else: l_kwds["metavar"] = "ARG" if c_opt.m_default != None: l_kwds["help"] += " [default:%s]" % str(c_opt.m_default) l_args.append(c_opt.m_longopt) l_group.add_option(*l_args, **l_kwds) self.m_cmdParser.add_option_group(l_group) def _cmd_parser_load(self, p_argv): self.m_cmdOpts, self.m_cmdArgs = self.m_cmdParser.parse_args(p_argv) for c_option in [ x for x in self.m_options if x.m_cmdline ]: l_attribute = self._cmd_attribute_name(c_option.m_section, c_option.m_name) l_value = getattr(self.m_cmdOpts, l_attribute) if l_value != None: l_value = self._validate(c_option.m_section, c_option.m_name, l_value) self.set(c_option.m_section, c_option.m_name, l_value) elif c_option.m_mandatory: raise ConfigValueError(c_option.m_section, c_option.m_name, "option is mandatory")
[docs] def option_cmdline_given(self, p_section, p_option): if self.option_exists(p_section, p_option): l_name = self._cmd_attribute_name(p_section, p_option) l_value = getattr(self.m_cmdOpts, l_name) return l_value != None return False
def _file_parser_load(self): if not self.section_exists("general") or not self.option_exists("general", "config-file"): return l_fileName = self._validate("general", "config-file") try: with open(l_fileName, mode="r", encoding="utf-8") as l_file: l_lines = [ x for x in l_file.readlines() if not re.match(r"^\s*//.*" ,x) ] l_content = "\n".join(l_lines) l_data = json.loads(l_content) except Exception as l_error: l_message = "invalid json configuration : %s" % str(l_error) raise ConfigValueError("general", "config-file", l_message) for c_section, c_data in l_data.items(): for c_option, c_value in c_data.items(): l_option = self._get_option(c_section, c_option) if l_option.m_config and not self.option_cmdline_given(c_section, c_option): l_value = self._validate(c_section, c_option, c_value) self.set(c_section, c_option, l_value) def _validate(self, p_section, p_name, p_value = None): if p_value is None: p_value = self.get(p_section, p_name) l_option = self._get_option(p_section, p_name) return l_option.validate(p_value)
# Local Variables: # ispell-local-dictionary: "american" # End: