diff options
author | Marcello Stanisci <stanisci.m@gmail.com> | 2019-03-11 23:35:37 +0100 |
---|---|---|
committer | Marcello Stanisci <stanisci.m@gmail.com> | 2019-03-11 23:35:37 +0100 |
commit | c2ae44a4b22079b3ebcffe8bc507117d383669a6 (patch) | |
tree | a816094615ca23a74e3b47c248322e6fe28a8437 /talerbank/talerconfig.py | |
parent | 5b39318648d445a9903c3657549b60f9ca179ce9 (diff) | |
download | bank-c2ae44a4b22079b3ebcffe8bc507117d383669a6.tar.gz bank-c2ae44a4b22079b3ebcffe8bc507117d383669a6.tar.bz2 bank-c2ae44a4b22079b3ebcffe8bc507117d383669a6.zip |
Doxygen-commenting the config logic.
Diffstat (limited to 'talerbank/talerconfig.py')
-rw-r--r-- | talerbank/talerconfig.py | 292 |
1 files changed, 249 insertions, 43 deletions
diff --git a/talerbank/talerconfig.py b/talerbank/talerconfig.py index 8272e21..69d06a8 100644 --- a/talerbank/talerconfig.py +++ b/talerbank/talerconfig.py @@ -1,22 +1,21 @@ -# This file is part of TALER -# (C) 2016 INRIA +## +# This file is part of TALER +# (C) 2016 INRIA # -# TALER is free software; you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free Software -# Foundation; either version 3, or (at your option) any later version. +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. # -# TALER is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License along with -# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # -# @author Florian Dold - -""" -Parse GNUnet-style configurations in pure Python -""" +# @author Florian Dold +# @author Marcello Stanisci +# @brief Parse GNUnet-style configurations in pure Python import logging import collections @@ -31,6 +30,7 @@ LOGGER = logging.getLogger(__name__) __all__ = ["TalerConfig"] TALER_DATADIR = None + try: # not clear if this is a good idea ... from talerpaths import TALER_DATADIR as t @@ -38,21 +38,34 @@ try: except ImportError: pass +## +# Exception class for a any configuration error. class ConfigurationError(Exception): pass +## +# Exception class for malformed strings having with parameter +# expansion. class ExpansionSyntaxError(Exception): pass - +## +# Do shell-style parameter expansion. +# Supported syntax: +# - ${X} +# - ${X:-Y} +# - $X +# +# @param var entire config value that might contain a parameter +# to expand. +# @param getter function that is in charge of returning _some_ +# value to be used in place of the parameter to expand. +# Typically, the replacement is searched first under the +# PATHS section of the current configuration, or (if not +# found) in the environment. +# +# @return the expanded config value. def expand(var: str, getter: Callable[[str], str]) -> str: - """ - Do shell-style parameter expansion. - Supported syntax: - - ${X} - - ${X:-Y} - - $X - """ pos = 0 result = "" while pos != -1: @@ -89,11 +102,24 @@ def expand(var: str, getter: Callable[[str], str]) -> str: result = result + replace pos = end - return result + var[pos:] - +## +# A configuration entry. class Entry: + + ## + # Init constructor. + # + # @param self the object itself. + # @param config reference to a configuration object - FIXME + # define "configuration object". + # @param section name of the config section where this entry + # got defined. + # @param option name of the config option associated with this + # entry. + # @param kwargs keyword arguments that hold the value / filename + # / line number of this current option. def __init__(self, config, section: str, option: str, **kwargs) -> None: self.value = kwargs.get("value") self.filename = kwargs.get("filename") @@ -102,13 +128,35 @@ class Entry: self.option = option self.config = weakref.ref(config) + ## + # XML representation of this entry. + # + # @param self the object itself. + # @return XML string holding all the relevant information + # for this entry. def __repr__(self) -> str: return "<Entry section=%s, option=%s, value=%s>" \ % (self.section, self.option, repr(self.value),) + ## + # Return the value for this entry, as is. + # + # @param self the object itself. + # @return the config value. def __str__(self) -> Any: return self.value + ## + # Return entry value, accepting defaults. + # + # @param self the object itself + # @param default default value to return if none was found. + # @param required indicate whether the value was required or not. + # If the value was required, but was not found, an exception + # is found. + # @param warn if True, outputs a warning message if the value was + # not found -- regardless of it being required or not. + # @return the value, or the given @a default, if not found. def value_string(self, default=None, required=False, warn=False) -> str: if required and self.value is None: raise ConfigurationError("Missing required option '%s' in section '%s'" \ @@ -124,6 +172,15 @@ class Entry: return default return self.value + ## + # Return entry value as a _int_. Raise exception if the + # value is not convertible to a integer. + # + # @param self the object itself + # @param default currently ignored. + # @param required currently ignored. + # @param warn currently ignored. + # @return the value, or the given @a default, if not found. def value_int(self, default=None, required=False, warn=False) -> int: value = self.value_string(default, warn, required) if value is None: @@ -133,7 +190,12 @@ class Entry: except ValueError: raise ConfigurationError("Expected number for option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) - + ## + # Fetch value to substitute to expansion variables. + # + # @param self the object itself. + # @param key the value's name to lookup. + # @return the value, if found, None otherwise. def _getsubst(self, key: str) -> Any: value = self.config()["paths"][key].value if value is not None: @@ -143,57 +205,136 @@ class Entry: return value return None + ## + # Fetch the config value that should be a filename, + # taking care of invoking the variable-expansion logic first. + # + # @param self the object itself. + # @param default currently ignored. + # @param required currently ignored. + # @param warn currently ignored. + # @return the (expanded) filename. def value_filename(self, default=None, required=False, warn=False) -> str: value = self.value_string(default, required, warn) if value is None: return None return expand(value, self._getsubst) + ## + # Give the filename and line number of this config entry. + # + # @param self this object. + # @return <filename>:<linenumber>, or "<unknown>" if one + # is not known. def location(self) -> str: if self.filename is None or self.lineno is None: return "<unknown>" return "%s:%s" % (self.filename, self.lineno) - +## +# Represent a section by inheriting from 'defaultdict'. class OptionDict(collections.defaultdict): + + ## + # Init constructor. + # + # @param self the object itself + # @param config the "config" object -- typically a @a TalerConfig instance. + # @param section_name the section name to assign to this object. def __init__(self, config, section_name: str) -> None: self.config = weakref.ref(config) self.section_name = section_name super().__init__() + + ## + # Logic to run when a non-existent key is dereferenced. + # Just create and return a empty config @a Entry. Note + # that the freshly created entry will nonetheless put + # under the accessed key (that *does* become existent + # afterwards). + # + # @param self the object itself. + # @param key the key attempted to be accessed. + # @return the no-value entry. def __missing__(self, key: str) -> Entry: entry = Entry(self.config(), self.section_name, key) self[key] = entry return entry + + ## + # Attempt to fetch one value from the object. + # + # @param self the object itself. + # @param chunk the key (?) that is tried to access. + # @return the object, if it exists, or a freshly created + # (empty) one, if it doesn't exist. def __getitem__(self, chunk: str) -> Entry: return super().__getitem__(chunk.lower()) + + ## + # Set one value into the object. + # + # @param self the object itself. + # @param chunk key under which the value is going to be set. + # @param value value to set the @a chunk to. def __setitem__(self, chunk: str, value: Entry) -> None: super().__setitem__(chunk.lower(), value) - +## +# Collection of all the (@a OptionDict) sections. class SectionDict(collections.defaultdict): + + ## + # Automatically invoked when a missing section is + # dereferenced. It creates the missing - empty - section. + # + # @param self the object itself. + # @param key the dereferenced section name. + # @return the freshly created section. def __missing__(self, key): value = OptionDict(self, key) self[key] = value return value + + ## + # Attempt to retrieve a section. + # + # @param self the object itself. + # @param chunk the section name. def __getitem__(self, chunk: str) -> OptionDict: return super().__getitem__(chunk.lower()) + + ## + # Set a section. + # + # @param self the object itself. + # @param chunk the section name to set. + # @param value the value to set under that @a chunk. def __setitem__(self, chunk: str, value: OptionDict) -> None: super().__setitem__(chunk.lower(), value) - +## +# One loaded taler configuration, including base configuration +# files and included files. class TalerConfig: - """ - One loaded taler configuration, including base configuration - files and included files. - """ + + ## + # Init constructor.. + # + # @param self the object itself. def __init__(self) -> None: - """ - Initialize an empty configuration - """ self.sections = SectionDict() # just plain dict - # defaults != config file: the first is the 'base' - # whereas the second overrides things from the first. + ## + # Load a configuration file, instantiating a config object. + # + # @param filename the filename where to load the configuration + # from. If None, it defaults "taler.conf". + # @param load_defaults if True, then defaults values are loaded + # (from canonical directories like "<prefix>/share/config.d/taler/") + # before the actual configuration file. This latter then + # can override some/all the defaults. + # @return the config object. @staticmethod def from_file(filename=None, load_defaults=True): cfg = TalerConfig() @@ -208,18 +349,53 @@ class TalerConfig: cfg.load_file(os.path.expanduser(filename)) return cfg + ## + # Get a string value from the config. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted string (or a default / exception if + # a error occurs). def value_string(self, section, option, **kwargs) -> str: return self.sections[section][option].value_string( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) + ## + # Get a value from the config that should be a filename. + # The variable expansion for the path's components is internally managed. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted filename (or a default / exception if + # a error occurs). def value_filename(self, section, option, **kwargs) -> str: return self.sections[section][option].value_filename( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) + ## + # Get a integer value from the config. + # + # @param self the config object itself. + # @param section the section to fetch the value from. + # @param option the value's option name. + # @param kwargs dict argument with instructions about + # the value retrieval logic. + # @return the wanted integer (or a default / exception if + # a error occurs). def value_int(self, section, option, **kwargs) -> int: return self.sections[section][option].value_int( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) + ## + # Load default values from canonical locations. + # + # @param self the object itself. def load_defaults(self) -> None: base_dir = os.environ.get("TALER_BASE_CONFIG") if base_dir: @@ -237,15 +413,24 @@ class TalerConfig: return LOGGER.warning("no base directory found") + ## + # Load configuration from environment variable + # TALER_CONFIG_FILE or from default location if the + # variable is not set. + # + # @param args currently unused. + # @param kwargs kwargs for subroutine @a from_file. + # @return freshly instantiated config object. @staticmethod def from_env(*args, **kwargs): - """ - Load configuration from environment variable TALER_CONFIG_FILE - or from default location if the variable is not set. - """ filename = os.environ.get("TALER_CONFIG_FILE") return TalerConfig.from_file(filename, *args, **kwargs) + ## + # Load config values from _each_ file found in a directory. + # + # @param self the object itself. + # @param dirname the directory to crawl in the look for config files. def load_dir(self, dirname) -> None: try: files = os.listdir(dirname) @@ -257,6 +442,10 @@ class TalerConfig: continue self.load_file(os.path.join(dirname, file)) + ## + # Load config values from a file. + # + # @param filename config file to take the values from. def load_file(self, filename) -> None: sections = self.sections try: @@ -310,7 +499,15 @@ class TalerConfig: LOGGER.error("Configuration file (%s) not found", filename) sys.exit(3) - + ## + # Dump the textual representation of a config object. + # + # Format: + # + # [section] + # option = value # FIXME (what is location?) + # + # @param self the object itself, that will be dumped. def dump(self) -> None: for kv_section in self.sections.items(): print("[%s]" % (kv_section[1].section_name,)) @@ -320,6 +517,15 @@ class TalerConfig: kv_option[1].value, kv_option[1].location())) + + ## + # Return a whole section from this object. + # + # @param self the object itself. + # @param chunk name of the section to return. + # @return the section - note that if the section is + # not found, a empty one will created on the fly, + # then set under 'chunk', and returned. def __getitem__(self, chunk: str) -> OptionDict: if isinstance(chunk, str): return self.sections[chunk] |