From f7829956ff2ee4b517f010a3f23bc62012e881e7 Mon Sep 17 00:00:00 2001 From: Marcello Stanisci Date: Mon, 27 May 2019 18:03:08 +0200 Subject: use latest config logic --- talerdonations/talerconfig.py | 392 ++++++++++++++++++++++++++++++++---------- 1 file changed, 304 insertions(+), 88 deletions(-) (limited to 'talerdonations') diff --git a/talerdonations/talerconfig.py b/talerdonations/talerconfig.py index a7ca065..2d2c78e 100644 --- a/talerdonations/talerconfig.py +++ b/talerdonations/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 +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see # -# @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 @@ -24,34 +23,48 @@ import os import weakref import sys import re +from typing import Callable, Any 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 TALER_DATADIR = t 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 - -def expand(var, getter): - """ - Do shell-style parameter expansion. - Supported syntax: - - ${X} - - ${X:-Y} - - $X - """ +## +# 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: pos = 0 result = "" while pos != -1: @@ -88,38 +101,25 @@ def expand(var, getter): result = result + replace pos = end - return result + var[pos:] - -class OptionDict(collections.defaultdict): - def __init__(self, config, section_name): - self.config = weakref.ref(config) - self.section_name = section_name - super().__init__() - def __missing__(self, key): - entry = Entry(self.config(), self.section_name, key) - self[key] = entry - return entry - def __getitem__(self, chunk): - return super().__getitem__(chunk.lower()) - def __setitem__(self, chunk, value): - super().__setitem__(chunk.lower(), value) - - -class SectionDict(collections.defaultdict): - def __missing__(self, key): - value = OptionDict(self, key) - self[key] = value - return value - def __getitem__(self, chunk): - return super().__getitem__(chunk.lower()) - def __setitem__(self, chunk, value): - super().__setitem__(chunk.lower(), value) - - +## +# A configuration entry. class Entry: - def __init__(self, config, section, option, **kwargs): + + ## + # 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") self.lineno = kwargs.get("lineno") @@ -127,14 +127,36 @@ class Entry: self.option = option self.config = weakref.ref(config) - def __repr__(self): + ## + # 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 "" \ % (self.section, self.option, repr(self.value),) - def __str__(self): + ## + # Return the value for this entry, as is. + # + # @param self the object itself. + # @return the config value. + def __str__(self) -> Any: return self.value - def value_string(self, default=None, required=False, warn=False): + ## + # 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'" \ % (self.option.upper(), self.section.upper())) @@ -149,7 +171,16 @@ class Entry: return default return self.value - def value_int(self, default=None, required=False, warn=False): + ## + # 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: return None @@ -158,8 +189,13 @@ class Entry: except ValueError: raise ConfigurationError("Expected number for option '%s' in section '%s'" \ % (self.option.upper(), self.section.upper())) - - def _getsubst(self, key): + ## + # 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: return value @@ -168,31 +204,136 @@ class Entry: return value return None - def value_filename(self, default=None, required=False, warn=False): + ## + # 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) - def location(self): + ## + # Give the filename and line number of this config entry. + # + # @param self this object. + # @return :, or "" if one + # is not known. + def location(self) -> str: if self.filename is None or self.lineno is None: return "" 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. - """ - def __init__(self): - """ - Initialize an empty configuration - """ - self.sections = SectionDict() - - # defaults != config file: the first is the 'base' - # whereas the second overrides things from the first. + + ## + # Init constructor.. + # + # @param self the object itself. + def __init__(self) -> None: + self.sections = SectionDict() # just plain dict + + ## + # 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 "/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() @@ -204,22 +345,57 @@ class TalerConfig: filename = os.path.expanduser("~/.config/taler.conf") if load_defaults: cfg.load_defaults() - cfg.load_file(filename) + cfg.load_file(os.path.expanduser(filename)) return cfg - def value_string(self, section, option, **kwargs): + ## + # 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")) - def value_filename(self, section, option, **kwargs): + ## + # 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")) - def value_int(self, section, option, **kwargs): + ## + # 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")) - def load_defaults(self): + ## + # 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: self.load_dir(base_dir) @@ -236,16 +412,25 @@ 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) - def load_dir(self, dirname): + ## + # 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) except FileNotFoundError: @@ -256,7 +441,11 @@ class TalerConfig: continue self.load_file(os.path.join(dirname, file)) - def load_file(self, filename): + ## + # 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: with open(filename, "r") as file: @@ -271,6 +460,16 @@ class TalerConfig: if line.startswith("#"): # comment continue + if line.startswith("@INLINE@"): + pair = line.split() + if 2 != len(pair): + LOGGER.error("invalid inlined config filename given ('%s')" % line) + continue + if pair[1].startswith("/"): + self.load_file(pair[1]) + else: + self.load_file(os.path.join(os.path.dirname(filename), pair[1])) + continue if line.startswith("["): if not line.endswith("]"): LOGGER.error("invalid section header in line %s: %s", @@ -299,8 +498,16 @@ class TalerConfig: LOGGER.error("Configuration file (%s) not found", filename) sys.exit(3) - - def dump(self): + ## + # 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,)) for kv_option in kv_section[1].items(): @@ -309,7 +516,16 @@ class TalerConfig: kv_option[1].value, kv_option[1].location())) - def __getitem__(self, chunk): + + ## + # 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] raise TypeError("index must be string") -- cgit v1.2.3