# 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 """ Parse GNUnet-style configurations in pure Python """ import logging import collections import os import weakref import sys import re LOGGER = logging.getLogger(__name__) __all__ = ["Config"] class ConfigurationError(Exception): pass class ExpansionSyntaxError(Exception): pass 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) class Entry: def __init__(self, config, section, option, **kwargs): 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) def __repr__(self): return "" \ % (self.section, self.option, repr(self.value),) def __str__(self): return self.value def value_string(self, default=None, required=False, warn=False): 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 def value_int(self, default=None, required=False, warn=False): 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())) def _getsubst(self, key): 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 def value_filename(self, default=None, required=False, warn=False): value = self.value_string(default, required, warn) if value is None: return None return expand(value, self._getsubst) def location(self): if self.filename is None or self.lineno is None: return "" return "%s:%s" % (self.filename, self.lineno) class Config: """ 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. @staticmethod def from_file(filename=None, load_defaults=True): cfg = Config() cfg.load_file(filename) return cfg def value_string(self, section, option, **kwargs): return self.sections[section][option].value_string( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) def value_filename(self, section, option, **kwargs): return self.sections[section][option].value_filename( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) def value_int(self, section, option, **kwargs): return self.sections[section][option].value_int( kwargs.get("default"), kwargs.get("required"), kwargs.get("warn")) def load_defaults(self): base_dir = os.path.dirname(os.path.realpath(__file__)) if base_dir: self.load_dir(base_dir) return LOGGER.warning("no base directory found") @staticmethod def from_env(*args, **kwargs): """ Load configuration """ return Config.from_file('codeless.conf', *args, **kwargs) def load_dir(self, dirname): 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)) def load_file(self, filename): 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("["): 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) def dump(self): 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())) def __getitem__(self, chunk): 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 = Config.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()