From ca937d2e6ed6d793221d0e10007be6443b0cf045 Mon Sep 17 00:00:00 2001 From: ng0 Date: Mon, 23 Sep 2019 19:06:25 +0000 Subject: rename. --- .gitignore | 1 + taler/__init__.py | 0 taler/amount/README | 4 - taler/amount/amount.py | 156 ----------- taler/amount/test_amount.py | 78 ------ taler/config/talerconfig.py | 560 --------------------------------------- taler/log/README | 1 - taler/log/gnunet_log.py | 271 ------------------- taler/log/test.py | 239 ----------------- taler/util/__init__.py | 0 taler/util/amount/README | 4 + taler/util/amount/__init__.py | 0 taler/util/amount/amount.py | 156 +++++++++++ taler/util/amount/test_amount.py | 78 ++++++ taler/util/config/__init__.py | 0 taler/util/config/talerconfig.py | 560 +++++++++++++++++++++++++++++++++++++++ taler/util/log/README | 1 + taler/util/log/__init__.py | 0 taler/util/log/gnunet_log.py | 271 +++++++++++++++++++ taler/util/log/test.py | 239 +++++++++++++++++ 20 files changed, 1310 insertions(+), 1309 deletions(-) create mode 100644 taler/__init__.py delete mode 100644 taler/amount/README delete mode 100644 taler/amount/amount.py delete mode 100755 taler/amount/test_amount.py delete mode 100644 taler/config/talerconfig.py delete mode 100644 taler/log/README delete mode 100755 taler/log/gnunet_log.py delete mode 100755 taler/log/test.py create mode 100644 taler/util/__init__.py create mode 100644 taler/util/amount/README create mode 100644 taler/util/amount/__init__.py create mode 100644 taler/util/amount/amount.py create mode 100755 taler/util/amount/test_amount.py create mode 100644 taler/util/config/__init__.py create mode 100644 taler/util/config/talerconfig.py create mode 100644 taler/util/log/README create mode 100644 taler/util/log/__init__.py create mode 100755 taler/util/log/gnunet_log.py create mode 100755 taler/util/log/test.py diff --git a/.gitignore b/.gitignore index 3f4eecd..633e0fc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build/ dist/ *~ \#*\# +__pycache__ diff --git a/taler/__init__.py b/taler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taler/amount/README b/taler/amount/README deleted file mode 100644 index 2990bfe..0000000 --- a/taler/amount/README +++ /dev/null @@ -1,4 +0,0 @@ -Run the tests with: - -# Just from this directory. -$ python3 -m unittest diff --git a/taler/amount/amount.py b/taler/amount/amount.py deleted file mode 100644 index e4c4555..0000000 --- a/taler/amount/amount.py +++ /dev/null @@ -1,156 +0,0 @@ -# This file is part of TALER -# (C) 2017 TALER SYSTEMS -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later -# version. -# -# This library 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free -# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -# Boston, MA 02110-1301 USA -# -# @author Marcello Stanisci -# @version 0.1 -# @repository https://git.taler.net/copylib.git/ -# This code is "copylib", it is versioned under the Git repository -# mentioned above, and it is meant to be manually copied into -# any project which might need it. - -class CurrencyMismatch(Exception): - hint = "Internal logic error (currency mismatch)" - http_status_code = 500 - def __init__(self, curr1, curr2) -> None: - super(CurrencyMismatch, self).__init__( - "%s vs %s" % (curr1, curr2)) - -class BadFormatAmount(Exception): - hint = "Malformed amount string" - def __init__(self, faulty_str) -> None: - super(BadFormatAmount, self).__init__( - "Bad format amount: " + faulty_str) - -class NumberTooBig(Exception): - hint = "Number given is too big" - def __init__(self) -> None: - super(NumberTooBig, self).__init__( - "Number given is too big") - -class NegativeNumber(Exception): - hint = "Negative number given as value and/or fraction" - def __init__(self) -> None: - super(NegativeNumber, self).__init__( - "Negative number given as value and/or fraction") - -class Amount: - # How many "fraction" units make one "value" unit of currency - # (Taler requires 10^8). Do not change this 'constant'. - @staticmethod - def _fraction() -> int: - return 10 ** 8 - - @staticmethod - def _max_value() -> int: - return (2 ** 53) - 1 - - def __init__(self, currency, value=0, fraction=0) -> None: - if value < 0 or fraction < 0: - raise NegativeNumber() - self.value = value - self.fraction = fraction - self.currency = currency - self.__normalize() - if self.value > Amount._max_value(): - raise NumberTooBig() - - # Normalize amount - def __normalize(self) -> None: - if self.fraction >= Amount._fraction(): - self.value += int(self.fraction / Amount._fraction()) - self.fraction = self.fraction % Amount._fraction() - - # Parse a string matching the format "A:B.C" - # instantiating an amount object. - @classmethod - def parse(cls, amount_str: str): - exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$' - import re - parsed = re.search(exp, amount_str) - if not parsed: - raise BadFormatAmount(amount_str) - value = int(parsed.group(2)) - fraction = 0 - for i, digit in enumerate(parsed.group(3) or "0"): - fraction += int(int(digit) * (Amount._fraction() / 10 ** (i+1))) - return cls(parsed.group(1), value, fraction) - - # Comare two amounts, return: - # -1 if a < b - # 0 if a == b - # 1 if a > b - @staticmethod - def cmp(am1, am2) -> int: - if am1.currency != am2.currency: - raise CurrencyMismatch(am1.currency, am2.currency) - if am1.value == am2.value: - if am1.fraction < am2.fraction: - return -1 - if am1.fraction > am2.fraction: - return 1 - return 0 - if am1.value < am2.value: - return -1 - return 1 - - def set(self, currency: str, value=0, fraction=0) -> None: - self.currency = currency - self.value = value - self.fraction = fraction - - # Add the given amount to this one - def add(self, amount) -> None: - if self.currency != amount.currency: - raise CurrencyMismatch(self.currency, amount.currency) - self.value += amount.value - self.fraction += amount.fraction - self.__normalize() - - # Subtract passed amount from this one - def subtract(self, amount) -> None: - if self.currency != amount.currency: - raise CurrencyMismatch(self.currency, amount.currency) - if self.fraction < amount.fraction: - self.fraction += Amount._fraction() - self.value -= 1 - if self.value < amount.value: - raise ValueError('self is lesser than amount to be subtracted') - self.value -= amount.value - self.fraction -= amount.fraction - - # Dump string from this amount, will put 'ndigits' numbers - # after the dot. - def stringify(self, ndigits: int, pretty=False) -> str: - if ndigits <= 0: - raise BadFormatAmount("ndigits must be > 0") - tmp = self.fraction - fraction_str = "" - while ndigits > 0: - fraction_str += str(int(tmp / (Amount._fraction() / 10))) - tmp = (tmp * 10) % (Amount._fraction()) - ndigits -= 1 - if not pretty: - return "%s:%d.%s" % (self.currency, self.value, fraction_str) - return "%d.%s %s" % (self.value, fraction_str, self.currency) - - # Dump the Taler-compliant 'dict' amount - def dump(self) -> dict: - return dict(value=self.value, - fraction=self.fraction, - currency=self.currency) diff --git a/taler/amount/test_amount.py b/taler/amount/test_amount.py deleted file mode 100755 index 41b9037..0000000 --- a/taler/amount/test_amount.py +++ /dev/null @@ -1,78 +0,0 @@ -# This file is part of TALER -# (C) 2017 TALER SYSTEMS -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -# -# @author Marcello Stanisci -# @version 0.0 -# @repository https://git.taler.net/copylib.git/ -# This code is "copylib", it is versioned under the Git repository -# mentioned above, and it is meant to be manually copied into any project -# which might need it. - -from __future__ import unicode_literals -from amount import Amount, BadFormatAmount, NumberTooBig, NegativeNumber -from unittest import TestCase -import json -from mock import MagicMock - -class TestAmount(TestCase): - def setUp(self): - self.amount = Amount('TESTKUDOS') - - def test_very_big_number(self): - with self.assertRaises(NumberTooBig): - self.Amount = Amount('TESTKUDOS', - value=99999999999999999999999999999999999999999999) - - def test_negative_value(self): - with self.assertRaises(NegativeNumber): - self.Amount = Amount('TESTKUDOS', - value=-9) - - def test_bad_stringification(self): - amount = Amount('TESTKUDOS') - with self.assertRaises(BadFormatAmount): - amount.stringify(0) - - def test_parse_and_cmp(self): - a = self.amount.parse('TESTKUDOS:0.0') - self.assertEqual(Amount.cmp(self.amount, a), 0) - b = self.amount.parse('TESTKUDOS:0.1') - self.assertEqual(Amount.cmp(Amount('TESTKUDOS', fraction=10000000), b), 0) - c = self.amount.parse('TESTKUDOS:3.3') - self.assertEqual(Amount.cmp(Amount('TESTKUDOS', 3, 30000000), c), 0) - self.assertEqual(Amount.cmp(a, b), -1) - self.assertEqual(Amount.cmp(c, b), 1) - with self.assertRaises(BadFormatAmount): - Amount.parse(':3') - - def test_add_and_dump(self): - mocky = MagicMock() - self.amount.add(Amount('TESTKUDOS', 9, 10**8)) - mocky(**self.amount.dump()) - mocky.assert_called_with(currency='TESTKUDOS', value=10, fraction=0) - - def test_subtraction(self): - with self.assertRaises(ValueError): - self.amount.subtract(Amount('TESTKUDOS', fraction=1)) - a = Amount('TESTKUDOS', 2) - a.subtract(Amount('TESTKUDOS', 1, 99999999)) - self.assertEqual(Amount.cmp(a, Amount('TESTKUDOS', fraction=1)), 0) - - def test_stringify(self): - self.assertEqual(self.amount.stringify(3), 'TESTKUDOS:0.000') - self.amount.add(Amount('TESTKUDOS', 2, 100)) - self.assertEqual(self.amount.stringify(6), 'TESTKUDOS:2.000001') diff --git a/taler/config/talerconfig.py b/taler/config/talerconfig.py deleted file mode 100644 index 9598be5..0000000 --- a/taler/config/talerconfig.py +++ /dev/null @@ -1,560 +0,0 @@ -## -# 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 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 -# -# @author Florian Dold -# @author Marcello Stanisci -# @brief Parse GNUnet-style configurations in pure Python - -import logging -import collections -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 - -## -# 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: - start = var.find("$", pos) - if start == -1: - break - if var[start:].startswith("${"): - balance = 1 - end = start + 2 - while balance > 0 and end < len(var): - balance += {"{": 1, "}": -1}.get(var[end], 0) - end += 1 - if balance != 0: - raise ExpansionSyntaxError("unbalanced parentheses") - piece = var[start+2:end-1] - if piece.find(":-") > 0: - varname, alt = piece.split(":-", 1) - replace = getter(varname) - if replace is None: - replace = expand(alt, getter) - else: - varname = piece - replace = getter(varname) - if replace is None: - replace = var[start:end] - else: - end = start + 2 - while end < len(var) and var[start+1:end+1].isalnum(): - end += 1 - varname = var[start+1:end] - replace = getter(varname) - if replace is None: - replace = var[start:end] - 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") - self.lineno = kwargs.get("lineno") - self.section = section - 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 "" \ - % (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'" \ - % (self.option.upper(), self.section.upper())) - if self.value is None: - if warn: - if default is not None: - LOGGER.warning("Configuration is missing option '%s' in section '%s',\ - falling back to '%s'", self.option, self.section, default) - else: - LOGGER.warning("Configuration ** is missing option '%s' in section '%s'", - self.option.upper(), self.section.upper()) - 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: - return None - try: - return int(value) - 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: - return value - value = os.environ.get(key) - if value is not None: - 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 :, 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: - - ## - # 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() - if filename is None: - xdg = os.environ.get("XDG_CONFIG_HOME") - if xdg: - filename = os.path.join(xdg, "taler.conf") - else: - filename = os.path.expanduser("~/.config/taler.conf") - logging.info("Loading default config: (%s)" % filename) - if load_defaults: - cfg.load_defaults() - 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: - self.load_dir(base_dir) - return - prefix = os.environ.get("TALER_PREFIX") - if prefix: - tmp = os.path.split(os.path.normpath(prefix)) - if re.match("lib", tmp[1]): - prefix = tmp[0] - self.load_dir(os.path.join(prefix, "share/taler/config.d")) - return - if TALER_DATADIR: - self.load_dir(os.path.join(TALER_DATADIR, "share/taler/config.d")) - 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): - 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) - except FileNotFoundError: - LOGGER.warning("can't read config directory '%s'", dirname) - return - for file in files: - if not file.endswith(".conf"): - 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: - with open(filename, "r") as file: - lineno = 0 - current_section = None - for line in file: - lineno += 1 - line = line.strip() - if line == "": - # empty line - continue - 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", - lineno, repr(line)) - section_name = line.strip("[]").strip().strip('"') - current_section = section_name - continue - if current_section is None: - LOGGER.error("option outside of section in line %s: %s", lineno, repr(line)) - continue - pair = line.split("=", 1) - if len(pair) != 2: - LOGGER.error("invalid option in line %s: %s", lineno, repr(line)) - key = pair[0].strip() - value = pair[1].strip() - if value.startswith('"'): - value = value[1:] - if not value.endswith('"'): - LOGGER.error("mismatched quotes in line %s: %s", lineno, repr(line)) - else: - value = value[:-1] - entry = Entry(self.sections, current_section, key, - value=value, filename=filename, lineno=lineno) - sections[current_section][key] = entry - except FileNotFoundError: - 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,)) - for kv_option in kv_section[1].items(): - print("%s = %s # %s" % \ - (kv_option[1].option, - 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] - raise TypeError("index must be string") - - -if __name__ == "__main__": - import argparse - - PARSER = argparse.ArgumentParser() - PARSER.add_argument("--section", "-s", dest="section", - default=None, metavar="SECTION") - PARSER.add_argument("--option", "-o", dest="option", - default=None, metavar="OPTION") - PARSER.add_argument("--config", "-c", dest="config", - default=None, metavar="FILE") - PARSER.add_argument("--filename", "-f", dest="expand_filename", - default=False, action='store_true') - ARGS = PARSER.parse_args() - - TC = TalerConfig.from_file(ARGS.config) - - if ARGS.section is not None and ARGS.option is not None: - if ARGS.expand_filename: - X = TC.value_filename(ARGS.section, ARGS.option) - else: - X = TC.value_string(ARGS.section, ARGS.option) - if X is not None: - print(X) - else: - TC.dump() diff --git a/taler/log/README b/taler/log/README deleted file mode 100644 index 1757c34..0000000 --- a/taler/log/README +++ /dev/null @@ -1 +0,0 @@ -This directory should mainly make #4453 solved. diff --git a/taler/log/gnunet_log.py b/taler/log/gnunet_log.py deleted file mode 100755 index 5a7d0b2..0000000 --- a/taler/log/gnunet_log.py +++ /dev/null @@ -1,271 +0,0 @@ -## -# -# This file is part of TALER -# (C) 2014, 2015, 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 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 -# -# -# @author Marcello Stanisci -# @brief Implementation of the GNUnet logging logic. -# -#!/usr/bin/env python3 - -# GNUNET_FORCE_LOG format [component];[file];[function];[from line [to line]];loglevel - -import os -import re -import logging -import datetime -import inspect - - -## -# Represent a definition for one logging action. -class LogDefinition: - - ## - # Init constructor. - # - # @param self the object itself. - # @param component which component this definition is going to affect. - # @param filename which filename this definition is going to affect. - # @param function which function this definition is going to affect. - # @param line_interval which line_interval this definition is going to affect. - # @param loglevel which loglevel is accepted. - # @param forced does this definition come from GNUNET_FORCE_LOG? - def __init__(self, component, filename, function, line_interval, loglevel, forced): - self.forced = forced - self.component = ".*" if "" == component else component - self.filename = ".*" if "" == filename else filename - self.function = ".*" if "" == function else function - self.line_interval = self.__parse_line_interval(line_interval) - - # string here - self.loglevel = loglevel - - ## - # Parse the @a line_interval from a logging definition. - # - # @param self the object itself. - # @param line_interval the line interval value as it comes - # from the user definition. The format is X[-Y]. - # @return a dict with min/max fields; if max is not given, - # then min == max. If the input is wrong, then just - # match every line. - def __parse_line_interval(self, line_interval): - interval_re = "^([0-9]+)(-([0-9]+))?$" - match = re.match(interval_re, line_interval) - if match: - return dict(min=int(match.group(1)), - max=int(match.group(3) if match.group(3) else match.group(1))) - - # just match every line if bad interval was provided. - return dict(min=0, max=float("inf")) - -## -# Represent a loglevel. -# -# @param self the object itself. -# @param string the loglevel given as a string (DEBUG/ERROR/WARNING/...) -# @param level the loglevel given as for the 'logging' module API. -# @param function the native function from 'logging' module that -# _actually_ outputs the log line. -class GnunetLoglevel: - def __init__(self, string, level, function): - self.string = string - self.level = level - self.function = function - - def __str__(self): - return self.string - - def getLevel(self): - return self.level - - def getFunction(self): - return self.function - - -## -# The "mother" class of this module. This class is in charge of -# parsing the definitions given by the user, from all the streams: -# being it programmatical, or the environment. It is also in charge -# of giving the right precedence to the streams: e.g. GNUNET_FORCE_LOG -# takes precedence over the "setup()" method. -class GnunetLogger: - - COMPONENT_IDX = 0 - FILENAME_IDX = 1 - FUNCTION_IDX = 2 - LINE_INTERVAL = 3 - LEVEL_IDX = 4 - - ## - # Init contructor. - # - # @param self the object itself. - # @param component the component name, that will be fed - # to the native 'logging' API. - def __init__(self, component): - self.logger = logging.getLogger(component) - self.ERROR = GnunetLoglevel("ERROR", logging.ERROR, self.logger.error) - self.WARNING = GnunetLoglevel("WARNING", logging.WARNING, self.logger.warning) - self.INFO = GnunetLoglevel("INFO", logging.INFO, self.logger.info) - self.DEBUG = GnunetLoglevel("DEBUG", logging.DEBUG, self.logger.debug) - - self.component = component - self.loglevel = None - - # Setting the *logging* loglevel in order to have the - # chance of changing the *logger* (object) loglevel along the - # execution. So this particular loglevel has no relevance - # (might have been any other loglevel). - logging.basicConfig(level=logging.INFO) - - self.no_forced_definitions = True - self.definitions = list() - - # Give priority to the forced env. - if os.environ.get("GNUNET_FORCE_LOG"): - self.no_forced_definitions = False - self.__parse_definitions(os.environ.get("GNUNET_FORCE_LOG"), True) - - if os.environ.get("GNUNET_LOG"): - self.__parse_definitions(os.environ.get("GNUNET_LOG"), False) - - if os.environ.get("GNUNET_FORCE_LOGFILE"): - filename = self.parse_filename(os.environ.get("GNUNET_FORCE_LOGFILE")) - fh = logging.FileHandler(filename) - self.logger.addHandler(fh) - - - ## - # Parse the filename where to write log lines. - # - # @param self the object itself. - # @param filename the filename to parse (usually given - # to the '-l' option). - # @return the parse filename string (with all the dates - # placeholders interpreted.) - def parse_filename(self, filename): - # implement {} and [] substitution. - f = filename.replace("{}", self.component) - f = f.replace("[]", str(os.getpid())) - now = datetime.datetime.now() - f = f.replace("%Y", now.strftime("%Y")) - f = f.replace("%m", now.strftime("%m")) - f = f.replace("%d", now.strftime("%d")) - return f - - - ## - # Maps loglevels as strings, to loglevels as defined - # in _this_ object. - # - # @param self the object itself. - # @param level the string to map. - # @return the loglevel native of _this_ object; defaults - # to INFO, if not found in the map. - def string_to_loglevel(self, level): - level_map = { - "ERROR": self.ERROR, - "WARNING": self.WARNING, - "INFO": self.INFO, - "DEBUG": self.DEBUG} - - # Defaults to INFO. - return level_map.get(level, self.INFO) - - - ## - # Set the loglevel for this module. - def setup(self, loglevel): - self.loglevel = loglevel - - ## - # Log API for users to produce logs. - # - # @param self the object itself. - # @param message the message to log. - # @param message_loglevel the loglevel associated with the message. - def log(self, message, message_loglevel): - caller_frame = inspect.stack()[1] - - filename = os.path.basename(inspect.getfile(caller_frame[0])) - lineno = caller_frame.lineno - function = caller_frame.function - - # Ordinary case (level setup + nothing forced). - if self.loglevel and self.no_forced_definitions: - self.logger.setLevel(level=self.loglevel.getLevel()) - message_loglevel.getFunction()(message) - return - - # We crawl through GNUNET_FORCE_LOG definitions, - # or GNUNET_LOG (in case of non-forced definition - # and non-given loglevel at object creation time) - for defi in self.definitions: - if defi.forced or not self.loglevel: - if re.match(defi.component, self.component) \ - and re.match(defi.filename, filename) \ - and re.match(defi.function, function) \ - and defi.line_interval["min"] <= lineno <= defi.line_interval["max"]: - self.logger.setLevel(level=self.string_to_loglevel(defi.loglevel).getLevel()) - message_loglevel.getFunction()(message) - return - - # If the flow got here, then one of the following - # may hold. - # - # (1) GNUNET_FORCE_LOG was given and no definition was - # found for this component (neither forced nor unforced). - # Possibly, there also exists a default loglevel. - - if self.loglevel: - self.logger.setLevel( - level=self.loglevel.getLevel()) - - # (2) GNUNET_FORCE_LOG was NOT given and neither was - # a default loglevel, and also a unforced definition - # wasn't found. Must assign a made-up loglevel. - - else: - self.logger.setLevel(level=logging.INFO) - - message_loglevel.getFunction()(message) - - - ## - # Helper function that parses definitions coming from the environment. - # - # @param self the object itself. - # @param line the definition coming from the environment. - # @param forced whether the definition comes from GNUNET_FORCE_LOG or not. - def __parse_definitions(self, line, forced): - gfl_split = line.split("/") - for component in gfl_split: - gfl_split_split = component.split(";") - - if 5 != len(gfl_split_split): - print("warning: GNUNET_(FORCE_)LOG is malformed") - return - - definition = LogDefinition(gfl_split_split[GnunetLogger.COMPONENT_IDX], - gfl_split_split[GnunetLogger.FILENAME_IDX], - gfl_split_split[GnunetLogger.FUNCTION_IDX], - gfl_split_split[GnunetLogger.LINE_INTERVAL], - gfl_split_split[GnunetLogger.LEVEL_IDX], - forced) - - self.definitions.append(definition) diff --git a/taler/log/test.py b/taler/log/test.py deleted file mode 100755 index 90e12c1..0000000 --- a/taler/log/test.py +++ /dev/null @@ -1,239 +0,0 @@ -## -# This file is part of TALER -# (C) 2019 TALER SYSTEMS -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later -# version. -# -# This library 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free -# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -# Boston, MA 02110-1301 USA -# -# @author Marcello Stanisci -# @version 0.0 -# @repository https://git.taler.net/copylib.git/ -# @brief This code is "copylib", it is versioned under the Git -# repository mentioned above, and it is meant to be -# manually copied into any project which might need it. - -from unittest import TestCase -from gnunet_log import GnunetLogger as GL -import os -from mock import patch, MagicMock -import logging -from datetime import datetime - -# How the logging module defines loglevels. -# -# ERROR = 40 -# WARNING = 30 -# INFO = 20 -# DEBUG = 10 - - -## -# Helper function that removes any logging definition from the -# environment. -def clean_env(): - if os.environ.get("GNUNET_FORCE_LOG"): - del os.environ["GNUNET_FORCE_LOG"] - if os.environ.get("GNUNET_LOG"): - del os.environ["GNUNET_LOG"] - if os.environ.get("GNUNET_FORCE_LOGFILE"): - del os.environ["GNUNET_FORCE_LOGFILE"] - - - - -## -# "mother" class of all the tests. NOTE: no logs will appear -# on screen, as the setLevel function is mocked (therefore the -# level specified won't be made effective -- rather, only the -# default level (WARNING) will apply)! -class TestGnunetLog(TestCase): - ## - # Setup method; just take care of cleaning the environment. - def setUp(self): - clean_env() - - ## - # This function tests whether GNUNET_FORCE_LOGFILE - # is correctly interpreted. - # - # @param self the object itself. - # @param mocked_FileHandler "mock" object that will - # "pretend" to be the file handler where the - # logging logic will register the logfile path. - # @param mocked_addHandler "mock" object on which the - # logging logic is expected to register the @a - # mocked_FileHandler. - @patch("logging.Logger.addHandler") - @patch("logging.FileHandler") - def test_force_logfile(self, mocked_FileHandler, mocked_addHandler): - os.environ["GNUNET_FORCE_LOGFILE"] = "/tmp/{}-[]-%Y_%m_%d.log" - unused_mock = MagicMock() - mocked_FileHandler.return_value = unused_mock - gl = GL("gnunet-pylog") - gl.log("msg", gl.DEBUG) - - today = datetime.now() - expected_filename = "/tmp/gnunet-pylog-%s-%s.log" % (str(os.getpid()), today.strftime("%Y_%m_%d")) - mocked_FileHandler.assert_called_with(expected_filename) - mocked_addHandler.assert_called_with(unused_mock) - - ## - # This function tests the very basic case, where no - # env variable is set and no explicit loglevel is given - # via the "setup()" method. The expected result is that - # the level is set to INFO. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_no_env_and_no_setup(self, mocked_basicConfig, mocked_setLevel): - # Double-check no env variable gets in the way. - assert None == os.environ.get("GNUNET_FORCE_LOG") - assert None == os.environ.get("GNUNET_LOG") - gl = GL("gnunet-pylog") - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.INFO) - - - ## - # This function tests the case where *only* the GNUNET_LOG - # env variable is set -- not even the manual setup of the - # loglevel - via a call to the "setup()" method - is put in place. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_non_forced_env(self, mocked_basicConfig, mocked_setLevel): - assert None == os.environ.get("GNUNET_FORCE_LOG") - os.environ["GNUNET_LOG"] = "gnunet-pylog;test.py;test_non_forced_env;99;ERROR" # lineno is not 100% accurate. - gl = GL("gnunet-pylog") - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.ERROR) - - ## - # This function tests the case where *only* the GNUNET_FORCE_LOG - # env variable is set -- not even the manual setup of the loglevel - # is put in place. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_only_forced_env(self, mocked_basicConfig, mocked_setLevel): - assert None == os.environ.get("GNUNET_LOG") - os.environ["GNUNET_FORCE_LOG"] = "gnunet-pylog;test.py;test_only_forced_env;90-200;ERROR" - gl = GL("gnunet-pylog") - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.ERROR) - - ## - # This function tests the case where *only* the manual - # loglevel setup is put in place. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_only_manual_loglevel_setup(self, mocked_basicConfig, mocked_setLevel): - assert None == os.environ.get("GNUNET_LOG") - assert None == os.environ.get("GNUNET_FORCE_LOG") - gl = GL("gnunet-pylog") - gl.setup(gl.ERROR) - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.ERROR) - - - ## - # This function tests the case where *both* the manual loglevel - # and the forced env variable are setup; the expected result is - # that the forced variable wins over the manual setup. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_manual_loglevel_AND_forced_env(self, mocked_basicConfig, mocked_setLevel): - assert None == os.environ.get("GNUNET_LOG") - assert None == os.environ.get("GNUNET_FORCE_LOG") - - # forced env definition (*before* object creation) - os.environ["GNUNET_FORCE_LOG"] = ";;;;ERROR" - gl = GL("gnunet-pylog") - - # manual setup - gl.setup(gl.WARNING) - - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.ERROR) - - ## - # This function tests the case where *both* GNUNET_LOG and - # the manual loglevel setup are put in place. The expectation - # is that the manual loglevel wins. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_manual_loglevel_AND_nonforced_env(self, mocked_basicConfig, mocked_setLevel): - assert None == os.environ.get("GNUNET_LOG") - assert None == os.environ.get("GNUNET_FORCE_LOG") - os.environ["GNUNET_LOG"] = ";;;;DEBUG" - gl = GL("gnunet-pylog") - gl.setup(gl.ERROR) - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.ERROR) - - ## - # This function tests the case where *both* GNUNET_LOG and - # GNUNET_FORCE_LOG are defined. The expectation is that the - # forced variable wins. - # - # @param self the object itself. - # @param mocked_basicConfig "mock" object that substitutes - # the real basicConfig. - # @param mocked_setLevel "mock" object that substitutes - # the real setLevel. - @patch("logging.Logger.setLevel") - @patch("logging.basicConfig") - def test_forced_env_AND_nonforced_env(self, mocked_basicConfig, mocked_setLevel): - assert None == os.environ.get("GNUNET_LOG") - assert None == os.environ.get("GNUNET_FORCE_LOG") - os.environ["GNUNET_LOG"] = ";;;;DEBUG" - os.environ["GNUNET_FORCE_LOG"] = ";;;;ERROR" - gl = GL("gnunet-pylog") - gl.log("msg", gl.DEBUG) - mocked_setLevel.assert_called_with(level=logging.ERROR) diff --git a/taler/util/__init__.py b/taler/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taler/util/amount/README b/taler/util/amount/README new file mode 100644 index 0000000..2990bfe --- /dev/null +++ b/taler/util/amount/README @@ -0,0 +1,4 @@ +Run the tests with: + +# Just from this directory. +$ python3 -m unittest diff --git a/taler/util/amount/__init__.py b/taler/util/amount/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taler/util/amount/amount.py b/taler/util/amount/amount.py new file mode 100644 index 0000000..e4c4555 --- /dev/null +++ b/taler/util/amount/amount.py @@ -0,0 +1,156 @@ +# This file is part of TALER +# (C) 2017 TALER SYSTEMS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later +# version. +# +# This library 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci +# @version 0.1 +# @repository https://git.taler.net/copylib.git/ +# This code is "copylib", it is versioned under the Git repository +# mentioned above, and it is meant to be manually copied into +# any project which might need it. + +class CurrencyMismatch(Exception): + hint = "Internal logic error (currency mismatch)" + http_status_code = 500 + def __init__(self, curr1, curr2) -> None: + super(CurrencyMismatch, self).__init__( + "%s vs %s" % (curr1, curr2)) + +class BadFormatAmount(Exception): + hint = "Malformed amount string" + def __init__(self, faulty_str) -> None: + super(BadFormatAmount, self).__init__( + "Bad format amount: " + faulty_str) + +class NumberTooBig(Exception): + hint = "Number given is too big" + def __init__(self) -> None: + super(NumberTooBig, self).__init__( + "Number given is too big") + +class NegativeNumber(Exception): + hint = "Negative number given as value and/or fraction" + def __init__(self) -> None: + super(NegativeNumber, self).__init__( + "Negative number given as value and/or fraction") + +class Amount: + # How many "fraction" units make one "value" unit of currency + # (Taler requires 10^8). Do not change this 'constant'. + @staticmethod + def _fraction() -> int: + return 10 ** 8 + + @staticmethod + def _max_value() -> int: + return (2 ** 53) - 1 + + def __init__(self, currency, value=0, fraction=0) -> None: + if value < 0 or fraction < 0: + raise NegativeNumber() + self.value = value + self.fraction = fraction + self.currency = currency + self.__normalize() + if self.value > Amount._max_value(): + raise NumberTooBig() + + # Normalize amount + def __normalize(self) -> None: + if self.fraction >= Amount._fraction(): + self.value += int(self.fraction / Amount._fraction()) + self.fraction = self.fraction % Amount._fraction() + + # Parse a string matching the format "A:B.C" + # instantiating an amount object. + @classmethod + def parse(cls, amount_str: str): + exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$' + import re + parsed = re.search(exp, amount_str) + if not parsed: + raise BadFormatAmount(amount_str) + value = int(parsed.group(2)) + fraction = 0 + for i, digit in enumerate(parsed.group(3) or "0"): + fraction += int(int(digit) * (Amount._fraction() / 10 ** (i+1))) + return cls(parsed.group(1), value, fraction) + + # Comare two amounts, return: + # -1 if a < b + # 0 if a == b + # 1 if a > b + @staticmethod + def cmp(am1, am2) -> int: + if am1.currency != am2.currency: + raise CurrencyMismatch(am1.currency, am2.currency) + if am1.value == am2.value: + if am1.fraction < am2.fraction: + return -1 + if am1.fraction > am2.fraction: + return 1 + return 0 + if am1.value < am2.value: + return -1 + return 1 + + def set(self, currency: str, value=0, fraction=0) -> None: + self.currency = currency + self.value = value + self.fraction = fraction + + # Add the given amount to this one + def add(self, amount) -> None: + if self.currency != amount.currency: + raise CurrencyMismatch(self.currency, amount.currency) + self.value += amount.value + self.fraction += amount.fraction + self.__normalize() + + # Subtract passed amount from this one + def subtract(self, amount) -> None: + if self.currency != amount.currency: + raise CurrencyMismatch(self.currency, amount.currency) + if self.fraction < amount.fraction: + self.fraction += Amount._fraction() + self.value -= 1 + if self.value < amount.value: + raise ValueError('self is lesser than amount to be subtracted') + self.value -= amount.value + self.fraction -= amount.fraction + + # Dump string from this amount, will put 'ndigits' numbers + # after the dot. + def stringify(self, ndigits: int, pretty=False) -> str: + if ndigits <= 0: + raise BadFormatAmount("ndigits must be > 0") + tmp = self.fraction + fraction_str = "" + while ndigits > 0: + fraction_str += str(int(tmp / (Amount._fraction() / 10))) + tmp = (tmp * 10) % (Amount._fraction()) + ndigits -= 1 + if not pretty: + return "%s:%d.%s" % (self.currency, self.value, fraction_str) + return "%d.%s %s" % (self.value, fraction_str, self.currency) + + # Dump the Taler-compliant 'dict' amount + def dump(self) -> dict: + return dict(value=self.value, + fraction=self.fraction, + currency=self.currency) diff --git a/taler/util/amount/test_amount.py b/taler/util/amount/test_amount.py new file mode 100755 index 0000000..41b9037 --- /dev/null +++ b/taler/util/amount/test_amount.py @@ -0,0 +1,78 @@ +# This file is part of TALER +# (C) 2017 TALER SYSTEMS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci +# @version 0.0 +# @repository https://git.taler.net/copylib.git/ +# This code is "copylib", it is versioned under the Git repository +# mentioned above, and it is meant to be manually copied into any project +# which might need it. + +from __future__ import unicode_literals +from amount import Amount, BadFormatAmount, NumberTooBig, NegativeNumber +from unittest import TestCase +import json +from mock import MagicMock + +class TestAmount(TestCase): + def setUp(self): + self.amount = Amount('TESTKUDOS') + + def test_very_big_number(self): + with self.assertRaises(NumberTooBig): + self.Amount = Amount('TESTKUDOS', + value=99999999999999999999999999999999999999999999) + + def test_negative_value(self): + with self.assertRaises(NegativeNumber): + self.Amount = Amount('TESTKUDOS', + value=-9) + + def test_bad_stringification(self): + amount = Amount('TESTKUDOS') + with self.assertRaises(BadFormatAmount): + amount.stringify(0) + + def test_parse_and_cmp(self): + a = self.amount.parse('TESTKUDOS:0.0') + self.assertEqual(Amount.cmp(self.amount, a), 0) + b = self.amount.parse('TESTKUDOS:0.1') + self.assertEqual(Amount.cmp(Amount('TESTKUDOS', fraction=10000000), b), 0) + c = self.amount.parse('TESTKUDOS:3.3') + self.assertEqual(Amount.cmp(Amount('TESTKUDOS', 3, 30000000), c), 0) + self.assertEqual(Amount.cmp(a, b), -1) + self.assertEqual(Amount.cmp(c, b), 1) + with self.assertRaises(BadFormatAmount): + Amount.parse(':3') + + def test_add_and_dump(self): + mocky = MagicMock() + self.amount.add(Amount('TESTKUDOS', 9, 10**8)) + mocky(**self.amount.dump()) + mocky.assert_called_with(currency='TESTKUDOS', value=10, fraction=0) + + def test_subtraction(self): + with self.assertRaises(ValueError): + self.amount.subtract(Amount('TESTKUDOS', fraction=1)) + a = Amount('TESTKUDOS', 2) + a.subtract(Amount('TESTKUDOS', 1, 99999999)) + self.assertEqual(Amount.cmp(a, Amount('TESTKUDOS', fraction=1)), 0) + + def test_stringify(self): + self.assertEqual(self.amount.stringify(3), 'TESTKUDOS:0.000') + self.amount.add(Amount('TESTKUDOS', 2, 100)) + self.assertEqual(self.amount.stringify(6), 'TESTKUDOS:2.000001') diff --git a/taler/util/config/__init__.py b/taler/util/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taler/util/config/talerconfig.py b/taler/util/config/talerconfig.py new file mode 100644 index 0000000..9598be5 --- /dev/null +++ b/taler/util/config/talerconfig.py @@ -0,0 +1,560 @@ +## +# 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 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 +# +# @author Florian Dold +# @author Marcello Stanisci +# @brief Parse GNUnet-style configurations in pure Python + +import logging +import collections +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 + +## +# 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: + start = var.find("$", pos) + if start == -1: + break + if var[start:].startswith("${"): + balance = 1 + end = start + 2 + while balance > 0 and end < len(var): + balance += {"{": 1, "}": -1}.get(var[end], 0) + end += 1 + if balance != 0: + raise ExpansionSyntaxError("unbalanced parentheses") + piece = var[start+2:end-1] + if piece.find(":-") > 0: + varname, alt = piece.split(":-", 1) + replace = getter(varname) + if replace is None: + replace = expand(alt, getter) + else: + varname = piece + replace = getter(varname) + if replace is None: + replace = var[start:end] + else: + end = start + 2 + while end < len(var) and var[start+1:end+1].isalnum(): + end += 1 + varname = var[start+1:end] + replace = getter(varname) + if replace is None: + replace = var[start:end] + 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") + self.lineno = kwargs.get("lineno") + self.section = section + 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 "" \ + % (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'" \ + % (self.option.upper(), self.section.upper())) + if self.value is None: + if warn: + if default is not None: + LOGGER.warning("Configuration is missing option '%s' in section '%s',\ + falling back to '%s'", self.option, self.section, default) + else: + LOGGER.warning("Configuration ** is missing option '%s' in section '%s'", + self.option.upper(), self.section.upper()) + 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: + return None + try: + return int(value) + 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: + return value + value = os.environ.get(key) + if value is not None: + 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 :, 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: + + ## + # 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() + if filename is None: + xdg = os.environ.get("XDG_CONFIG_HOME") + if xdg: + filename = os.path.join(xdg, "taler.conf") + else: + filename = os.path.expanduser("~/.config/taler.conf") + logging.info("Loading default config: (%s)" % filename) + if load_defaults: + cfg.load_defaults() + 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: + self.load_dir(base_dir) + return + prefix = os.environ.get("TALER_PREFIX") + if prefix: + tmp = os.path.split(os.path.normpath(prefix)) + if re.match("lib", tmp[1]): + prefix = tmp[0] + self.load_dir(os.path.join(prefix, "share/taler/config.d")) + return + if TALER_DATADIR: + self.load_dir(os.path.join(TALER_DATADIR, "share/taler/config.d")) + 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): + 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) + except FileNotFoundError: + LOGGER.warning("can't read config directory '%s'", dirname) + return + for file in files: + if not file.endswith(".conf"): + 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: + with open(filename, "r") as file: + lineno = 0 + current_section = None + for line in file: + lineno += 1 + line = line.strip() + if line == "": + # empty line + continue + 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", + lineno, repr(line)) + section_name = line.strip("[]").strip().strip('"') + current_section = section_name + continue + if current_section is None: + LOGGER.error("option outside of section in line %s: %s", lineno, repr(line)) + continue + pair = line.split("=", 1) + if len(pair) != 2: + LOGGER.error("invalid option in line %s: %s", lineno, repr(line)) + key = pair[0].strip() + value = pair[1].strip() + if value.startswith('"'): + value = value[1:] + if not value.endswith('"'): + LOGGER.error("mismatched quotes in line %s: %s", lineno, repr(line)) + else: + value = value[:-1] + entry = Entry(self.sections, current_section, key, + value=value, filename=filename, lineno=lineno) + sections[current_section][key] = entry + except FileNotFoundError: + 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,)) + for kv_option in kv_section[1].items(): + print("%s = %s # %s" % \ + (kv_option[1].option, + 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] + raise TypeError("index must be string") + + +if __name__ == "__main__": + import argparse + + PARSER = argparse.ArgumentParser() + PARSER.add_argument("--section", "-s", dest="section", + default=None, metavar="SECTION") + PARSER.add_argument("--option", "-o", dest="option", + default=None, metavar="OPTION") + PARSER.add_argument("--config", "-c", dest="config", + default=None, metavar="FILE") + PARSER.add_argument("--filename", "-f", dest="expand_filename", + default=False, action='store_true') + ARGS = PARSER.parse_args() + + TC = TalerConfig.from_file(ARGS.config) + + if ARGS.section is not None and ARGS.option is not None: + if ARGS.expand_filename: + X = TC.value_filename(ARGS.section, ARGS.option) + else: + X = TC.value_string(ARGS.section, ARGS.option) + if X is not None: + print(X) + else: + TC.dump() diff --git a/taler/util/log/README b/taler/util/log/README new file mode 100644 index 0000000..1757c34 --- /dev/null +++ b/taler/util/log/README @@ -0,0 +1 @@ +This directory should mainly make #4453 solved. diff --git a/taler/util/log/__init__.py b/taler/util/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taler/util/log/gnunet_log.py b/taler/util/log/gnunet_log.py new file mode 100755 index 0000000..5a7d0b2 --- /dev/null +++ b/taler/util/log/gnunet_log.py @@ -0,0 +1,271 @@ +## +# +# This file is part of TALER +# (C) 2014, 2015, 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 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 +# +# +# @author Marcello Stanisci +# @brief Implementation of the GNUnet logging logic. +# +#!/usr/bin/env python3 + +# GNUNET_FORCE_LOG format [component];[file];[function];[from line [to line]];loglevel + +import os +import re +import logging +import datetime +import inspect + + +## +# Represent a definition for one logging action. +class LogDefinition: + + ## + # Init constructor. + # + # @param self the object itself. + # @param component which component this definition is going to affect. + # @param filename which filename this definition is going to affect. + # @param function which function this definition is going to affect. + # @param line_interval which line_interval this definition is going to affect. + # @param loglevel which loglevel is accepted. + # @param forced does this definition come from GNUNET_FORCE_LOG? + def __init__(self, component, filename, function, line_interval, loglevel, forced): + self.forced = forced + self.component = ".*" if "" == component else component + self.filename = ".*" if "" == filename else filename + self.function = ".*" if "" == function else function + self.line_interval = self.__parse_line_interval(line_interval) + + # string here + self.loglevel = loglevel + + ## + # Parse the @a line_interval from a logging definition. + # + # @param self the object itself. + # @param line_interval the line interval value as it comes + # from the user definition. The format is X[-Y]. + # @return a dict with min/max fields; if max is not given, + # then min == max. If the input is wrong, then just + # match every line. + def __parse_line_interval(self, line_interval): + interval_re = "^([0-9]+)(-([0-9]+))?$" + match = re.match(interval_re, line_interval) + if match: + return dict(min=int(match.group(1)), + max=int(match.group(3) if match.group(3) else match.group(1))) + + # just match every line if bad interval was provided. + return dict(min=0, max=float("inf")) + +## +# Represent a loglevel. +# +# @param self the object itself. +# @param string the loglevel given as a string (DEBUG/ERROR/WARNING/...) +# @param level the loglevel given as for the 'logging' module API. +# @param function the native function from 'logging' module that +# _actually_ outputs the log line. +class GnunetLoglevel: + def __init__(self, string, level, function): + self.string = string + self.level = level + self.function = function + + def __str__(self): + return self.string + + def getLevel(self): + return self.level + + def getFunction(self): + return self.function + + +## +# The "mother" class of this module. This class is in charge of +# parsing the definitions given by the user, from all the streams: +# being it programmatical, or the environment. It is also in charge +# of giving the right precedence to the streams: e.g. GNUNET_FORCE_LOG +# takes precedence over the "setup()" method. +class GnunetLogger: + + COMPONENT_IDX = 0 + FILENAME_IDX = 1 + FUNCTION_IDX = 2 + LINE_INTERVAL = 3 + LEVEL_IDX = 4 + + ## + # Init contructor. + # + # @param self the object itself. + # @param component the component name, that will be fed + # to the native 'logging' API. + def __init__(self, component): + self.logger = logging.getLogger(component) + self.ERROR = GnunetLoglevel("ERROR", logging.ERROR, self.logger.error) + self.WARNING = GnunetLoglevel("WARNING", logging.WARNING, self.logger.warning) + self.INFO = GnunetLoglevel("INFO", logging.INFO, self.logger.info) + self.DEBUG = GnunetLoglevel("DEBUG", logging.DEBUG, self.logger.debug) + + self.component = component + self.loglevel = None + + # Setting the *logging* loglevel in order to have the + # chance of changing the *logger* (object) loglevel along the + # execution. So this particular loglevel has no relevance + # (might have been any other loglevel). + logging.basicConfig(level=logging.INFO) + + self.no_forced_definitions = True + self.definitions = list() + + # Give priority to the forced env. + if os.environ.get("GNUNET_FORCE_LOG"): + self.no_forced_definitions = False + self.__parse_definitions(os.environ.get("GNUNET_FORCE_LOG"), True) + + if os.environ.get("GNUNET_LOG"): + self.__parse_definitions(os.environ.get("GNUNET_LOG"), False) + + if os.environ.get("GNUNET_FORCE_LOGFILE"): + filename = self.parse_filename(os.environ.get("GNUNET_FORCE_LOGFILE")) + fh = logging.FileHandler(filename) + self.logger.addHandler(fh) + + + ## + # Parse the filename where to write log lines. + # + # @param self the object itself. + # @param filename the filename to parse (usually given + # to the '-l' option). + # @return the parse filename string (with all the dates + # placeholders interpreted.) + def parse_filename(self, filename): + # implement {} and [] substitution. + f = filename.replace("{}", self.component) + f = f.replace("[]", str(os.getpid())) + now = datetime.datetime.now() + f = f.replace("%Y", now.strftime("%Y")) + f = f.replace("%m", now.strftime("%m")) + f = f.replace("%d", now.strftime("%d")) + return f + + + ## + # Maps loglevels as strings, to loglevels as defined + # in _this_ object. + # + # @param self the object itself. + # @param level the string to map. + # @return the loglevel native of _this_ object; defaults + # to INFO, if not found in the map. + def string_to_loglevel(self, level): + level_map = { + "ERROR": self.ERROR, + "WARNING": self.WARNING, + "INFO": self.INFO, + "DEBUG": self.DEBUG} + + # Defaults to INFO. + return level_map.get(level, self.INFO) + + + ## + # Set the loglevel for this module. + def setup(self, loglevel): + self.loglevel = loglevel + + ## + # Log API for users to produce logs. + # + # @param self the object itself. + # @param message the message to log. + # @param message_loglevel the loglevel associated with the message. + def log(self, message, message_loglevel): + caller_frame = inspect.stack()[1] + + filename = os.path.basename(inspect.getfile(caller_frame[0])) + lineno = caller_frame.lineno + function = caller_frame.function + + # Ordinary case (level setup + nothing forced). + if self.loglevel and self.no_forced_definitions: + self.logger.setLevel(level=self.loglevel.getLevel()) + message_loglevel.getFunction()(message) + return + + # We crawl through GNUNET_FORCE_LOG definitions, + # or GNUNET_LOG (in case of non-forced definition + # and non-given loglevel at object creation time) + for defi in self.definitions: + if defi.forced or not self.loglevel: + if re.match(defi.component, self.component) \ + and re.match(defi.filename, filename) \ + and re.match(defi.function, function) \ + and defi.line_interval["min"] <= lineno <= defi.line_interval["max"]: + self.logger.setLevel(level=self.string_to_loglevel(defi.loglevel).getLevel()) + message_loglevel.getFunction()(message) + return + + # If the flow got here, then one of the following + # may hold. + # + # (1) GNUNET_FORCE_LOG was given and no definition was + # found for this component (neither forced nor unforced). + # Possibly, there also exists a default loglevel. + + if self.loglevel: + self.logger.setLevel( + level=self.loglevel.getLevel()) + + # (2) GNUNET_FORCE_LOG was NOT given and neither was + # a default loglevel, and also a unforced definition + # wasn't found. Must assign a made-up loglevel. + + else: + self.logger.setLevel(level=logging.INFO) + + message_loglevel.getFunction()(message) + + + ## + # Helper function that parses definitions coming from the environment. + # + # @param self the object itself. + # @param line the definition coming from the environment. + # @param forced whether the definition comes from GNUNET_FORCE_LOG or not. + def __parse_definitions(self, line, forced): + gfl_split = line.split("/") + for component in gfl_split: + gfl_split_split = component.split(";") + + if 5 != len(gfl_split_split): + print("warning: GNUNET_(FORCE_)LOG is malformed") + return + + definition = LogDefinition(gfl_split_split[GnunetLogger.COMPONENT_IDX], + gfl_split_split[GnunetLogger.FILENAME_IDX], + gfl_split_split[GnunetLogger.FUNCTION_IDX], + gfl_split_split[GnunetLogger.LINE_INTERVAL], + gfl_split_split[GnunetLogger.LEVEL_IDX], + forced) + + self.definitions.append(definition) diff --git a/taler/util/log/test.py b/taler/util/log/test.py new file mode 100755 index 0000000..90e12c1 --- /dev/null +++ b/taler/util/log/test.py @@ -0,0 +1,239 @@ +## +# This file is part of TALER +# (C) 2019 TALER SYSTEMS +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later +# version. +# +# This library 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301 USA +# +# @author Marcello Stanisci +# @version 0.0 +# @repository https://git.taler.net/copylib.git/ +# @brief This code is "copylib", it is versioned under the Git +# repository mentioned above, and it is meant to be +# manually copied into any project which might need it. + +from unittest import TestCase +from gnunet_log import GnunetLogger as GL +import os +from mock import patch, MagicMock +import logging +from datetime import datetime + +# How the logging module defines loglevels. +# +# ERROR = 40 +# WARNING = 30 +# INFO = 20 +# DEBUG = 10 + + +## +# Helper function that removes any logging definition from the +# environment. +def clean_env(): + if os.environ.get("GNUNET_FORCE_LOG"): + del os.environ["GNUNET_FORCE_LOG"] + if os.environ.get("GNUNET_LOG"): + del os.environ["GNUNET_LOG"] + if os.environ.get("GNUNET_FORCE_LOGFILE"): + del os.environ["GNUNET_FORCE_LOGFILE"] + + + + +## +# "mother" class of all the tests. NOTE: no logs will appear +# on screen, as the setLevel function is mocked (therefore the +# level specified won't be made effective -- rather, only the +# default level (WARNING) will apply)! +class TestGnunetLog(TestCase): + ## + # Setup method; just take care of cleaning the environment. + def setUp(self): + clean_env() + + ## + # This function tests whether GNUNET_FORCE_LOGFILE + # is correctly interpreted. + # + # @param self the object itself. + # @param mocked_FileHandler "mock" object that will + # "pretend" to be the file handler where the + # logging logic will register the logfile path. + # @param mocked_addHandler "mock" object on which the + # logging logic is expected to register the @a + # mocked_FileHandler. + @patch("logging.Logger.addHandler") + @patch("logging.FileHandler") + def test_force_logfile(self, mocked_FileHandler, mocked_addHandler): + os.environ["GNUNET_FORCE_LOGFILE"] = "/tmp/{}-[]-%Y_%m_%d.log" + unused_mock = MagicMock() + mocked_FileHandler.return_value = unused_mock + gl = GL("gnunet-pylog") + gl.log("msg", gl.DEBUG) + + today = datetime.now() + expected_filename = "/tmp/gnunet-pylog-%s-%s.log" % (str(os.getpid()), today.strftime("%Y_%m_%d")) + mocked_FileHandler.assert_called_with(expected_filename) + mocked_addHandler.assert_called_with(unused_mock) + + ## + # This function tests the very basic case, where no + # env variable is set and no explicit loglevel is given + # via the "setup()" method. The expected result is that + # the level is set to INFO. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_no_env_and_no_setup(self, mocked_basicConfig, mocked_setLevel): + # Double-check no env variable gets in the way. + assert None == os.environ.get("GNUNET_FORCE_LOG") + assert None == os.environ.get("GNUNET_LOG") + gl = GL("gnunet-pylog") + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.INFO) + + + ## + # This function tests the case where *only* the GNUNET_LOG + # env variable is set -- not even the manual setup of the + # loglevel - via a call to the "setup()" method - is put in place. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_non_forced_env(self, mocked_basicConfig, mocked_setLevel): + assert None == os.environ.get("GNUNET_FORCE_LOG") + os.environ["GNUNET_LOG"] = "gnunet-pylog;test.py;test_non_forced_env;99;ERROR" # lineno is not 100% accurate. + gl = GL("gnunet-pylog") + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.ERROR) + + ## + # This function tests the case where *only* the GNUNET_FORCE_LOG + # env variable is set -- not even the manual setup of the loglevel + # is put in place. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_only_forced_env(self, mocked_basicConfig, mocked_setLevel): + assert None == os.environ.get("GNUNET_LOG") + os.environ["GNUNET_FORCE_LOG"] = "gnunet-pylog;test.py;test_only_forced_env;90-200;ERROR" + gl = GL("gnunet-pylog") + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.ERROR) + + ## + # This function tests the case where *only* the manual + # loglevel setup is put in place. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_only_manual_loglevel_setup(self, mocked_basicConfig, mocked_setLevel): + assert None == os.environ.get("GNUNET_LOG") + assert None == os.environ.get("GNUNET_FORCE_LOG") + gl = GL("gnunet-pylog") + gl.setup(gl.ERROR) + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.ERROR) + + + ## + # This function tests the case where *both* the manual loglevel + # and the forced env variable are setup; the expected result is + # that the forced variable wins over the manual setup. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_manual_loglevel_AND_forced_env(self, mocked_basicConfig, mocked_setLevel): + assert None == os.environ.get("GNUNET_LOG") + assert None == os.environ.get("GNUNET_FORCE_LOG") + + # forced env definition (*before* object creation) + os.environ["GNUNET_FORCE_LOG"] = ";;;;ERROR" + gl = GL("gnunet-pylog") + + # manual setup + gl.setup(gl.WARNING) + + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.ERROR) + + ## + # This function tests the case where *both* GNUNET_LOG and + # the manual loglevel setup are put in place. The expectation + # is that the manual loglevel wins. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_manual_loglevel_AND_nonforced_env(self, mocked_basicConfig, mocked_setLevel): + assert None == os.environ.get("GNUNET_LOG") + assert None == os.environ.get("GNUNET_FORCE_LOG") + os.environ["GNUNET_LOG"] = ";;;;DEBUG" + gl = GL("gnunet-pylog") + gl.setup(gl.ERROR) + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.ERROR) + + ## + # This function tests the case where *both* GNUNET_LOG and + # GNUNET_FORCE_LOG are defined. The expectation is that the + # forced variable wins. + # + # @param self the object itself. + # @param mocked_basicConfig "mock" object that substitutes + # the real basicConfig. + # @param mocked_setLevel "mock" object that substitutes + # the real setLevel. + @patch("logging.Logger.setLevel") + @patch("logging.basicConfig") + def test_forced_env_AND_nonforced_env(self, mocked_basicConfig, mocked_setLevel): + assert None == os.environ.get("GNUNET_LOG") + assert None == os.environ.get("GNUNET_FORCE_LOG") + os.environ["GNUNET_LOG"] = ";;;;DEBUG" + os.environ["GNUNET_FORCE_LOG"] = ";;;;ERROR" + gl = GL("gnunet-pylog") + gl.log("msg", gl.DEBUG) + mocked_setLevel.assert_called_with(level=logging.ERROR) -- cgit v1.2.3