Source code for cfgrw

# pylint: disable=too-many-locals,too-few-public-methods
"""Python library for reading configuration properties from files and environment variables."""

import io
import os
from jinja2 import Environment, FileSystemLoader

from .formats.envvar import read_values as read_envvar_values
from .formats.ini import read_values as read_ini_values
from .formats.json import read_values as read_json_values
from .formats.xml import read_values as read_xml_values
from .formats.yaml import read_values as read_yaml_values

JINJA_EXT = ".j2"
"""str: File extension suffix that marks a Jinja2 template."""

CONF_FORMATS = {
    "ini": {"ext": [".ini"], "read_fn": read_ini_values},
    "json": {"ext": [".json"], "read_fn": read_json_values},
    "xml": {"ext": [".xml"], "read_fn": read_xml_values},
    "yaml": {"ext": [".yaml", ".yml"], "read_fn": read_yaml_values},
}
"""dict: Supported configuration formats mapped to their file extensions and reader functions."""


[docs] class CFGRW: """Reader for configuration properties from files and environment variables.""" def __init__(self, conf_file=None) -> None: """Initialise CFGRW. :param conf_file: Path to the configuration file. When ``None``, all properties are read from environment variables. :type conf_file: str, optional """ self.conf_file = conf_file self.conf_formats = CONF_FORMATS
[docs] def read( # pylint: disable=dangerous-default-value self, props: list, opts={} ) -> dict: """Read the values of a list of properties. When a configuration file is set, properties are read from it first. Any properties absent from the file are looked up as environment variables when ``prefix`` is provided in ``opts``. When no configuration file is set, all properties are read from environment variables. Properties not found in any source are silently omitted from the result. :param props: Property names to read. :type props: list[str] :param opts: Reader options. Recognised keys: - ``section`` *(str)* — section name, required for INI files. - ``prefix`` *(str)* — environment variable prefix, required for env var reads and env var fallback. :type opts: dict :returns: Mapping of property names to their values for all found properties. :rtype: dict """ if self.conf_file: if self.conf_file.endswith(JINJA_EXT): j2_template_dir = os.path.dirname(self.conf_file) j2_template_file = os.path.basename(self.conf_file) j2_file_loader = FileSystemLoader(j2_template_dir) j2_env = Environment(loader=j2_file_loader) template = j2_env.get_template(j2_template_file) conf_string = template.render({"env": dict(os.environ)}) conf_format = self._id_conf_format( self.conf_file.replace(JINJA_EXT, "") ) read_fn = conf_format["read_fn"] values = read_fn(io.StringIO(conf_string), props, opts) else: conf_format = self._id_conf_format(self.conf_file) read_fn = conf_format["read_fn"] with open(self.conf_file, "r", encoding="utf-8") as conf_stream: values = read_fn(conf_stream, props, opts) missing_props = [p for p in props if p not in values] if missing_props and "prefix" in opts: values.update(read_envvar_values(missing_props, opts)) else: values = read_envvar_values(props, opts) return values
def _id_conf_format(self, conf_file: str) -> dict: """Identify the configuration format settings for a given file path. :param conf_file: Path to the configuration file. :type conf_file: str :returns: Format settings dict with ``ext`` and ``read_fn`` keys, or ``None`` if no supported format matches. :rtype: dict or None """ for ( conf_format, # pylint: disable=unused-variable settings, ) in CONF_FORMATS.items(): if conf_file.endswith(tuple(settings["ext"])): return settings return None