Source code for smufolib.config

# pylint: disable=C0103, C0114
from __future__ import annotations
from typing import Any
import os
import importlib.resources
from urllib.parse import urlparse

from configparser import ConfigParser, ExtendedInterpolation
from pathlib import Path


[docs] def load(path: Path | str | None = None) -> dict[str, Any]: """Load parsed config file as :class:`dict`. If `path` is not provided, configuration is obtained by searching the standard locations described in the user guide (see :ref:`configuring-smufolib`). Files found later in the search order override earlier ones. If `path` is provided, only that file is loaded. :param path: Path to the `.cfg` configuration file. Defaults to :obj:`None`. Example: >>> from smufolib import config >>> cfg = config.load() >>> cfg["request"] {'encoding': 'utf-8', 'warn': True} """ config = _readConfigFile(path) parsed: dict[str, Any] = {} for section in config.sections(): parsed[section] = {} for option in config[section]: parsed[section][option] = _parse(config, section, option) return parsed
def _readConfigFile(path: Path | str | None) -> ConfigParser: # Read config file from selected filepath. config = ConfigParser(interpolation=ExtendedInterpolation()) config.optionxform = str # type: ignore defaultPath = importlib.resources.files("smufolib").joinpath("defaults.cfg") with defaultPath.open("r", encoding="utf-8") as defaults: config.read_file(defaults) userPaths = _selectPaths(path) parentPaths = [Path(str(defaultPath)).parent] + [p.parent for p in userPaths] config.read(userPaths, encoding="utf-8") _resolveMetadataPaths(config, parentPaths) return config def _selectPaths(path: Path | str | None) -> list[Path]: # Create a list of all possible filepaths in order of priority. nameExtension = ("smufolib", "cfg") envValue = os.getenv("_".join(nameExtension).upper()) candidates = [ Path.home() / ".".join(nameExtension), Path.cwd() / ".".join(nameExtension), ] if envValue: candidates.append(Path(envValue)) if path: candidates.append(Path(path)) return [p for p in candidates if Path(p).exists()] def _parse( config: ConfigParser, section: str, option: str ) -> str | int | float | bool | tuple[str | float, ...] | None: # Parse configured values. stringValue = config.get(section, option) try: return config.getint(section, option) except ValueError: try: return config.getfloat(section, option) except ValueError: try: return config.getboolean(section, option) except ValueError: if any((c in {")", "]"}) for c in stringValue): iterable = tuple(stringValue.strip(")][(").split(", ")) try: return tuple( float(i) if "." in i else int(i) if i.isdigit() else i for i in iterable ) except ValueError: return iterable # Strip \n to perserve multiline option after setting # config.optionxform = str. return stringValue.replace("\n", "") if stringValue else None def _resolveMetadataPaths(config: ConfigParser, baseDirs: list[Path]) -> None: # Resolve relative paths in [metadata] sections. for section in ("metadata.paths", "metadata.fallbacks"): if not config.has_section(section): continue for key, value in config.items(section): path = Path(value) if urlparse(value).scheme in {"http", "https"}: continue if path.is_absolute(): continue for base in reversed(baseDirs): candidate = base / path if candidate.exists(): config.set(section, key, str(candidate.resolve())) break