summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--talerdonations/talerconfig.py392
1 files changed, 304 insertions, 88 deletions
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 <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
@@ -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 "<Entry section=%s, option=%s, value=%s>" \
% (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 <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.
- """
- 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 "<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()
@@ -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")