Source code for pystow.config_api

"""Configuration handling."""

from __future__ import annotations

import os
from collections.abc import Callable
from configparser import ConfigParser
from functools import lru_cache
from pathlib import Path
from textwrap import dedent
from typing import Any, TypeVar

from .utils import getenv_path

__all__ = [
    "get_config",
    "write_config",
]

X = TypeVar("X")

CONFIG_NAME_ENVVAR = "PYSTOW_CONFIG_NAME"
CONFIG_HOME_ENVVAR = "PYSTOW_CONFIG_HOME"
CONFIG_NAME_DEFAULT = ".config"


[docs] class ConfigError(ValueError): """Raised when configuration can not be looked up.""" def __init__(self, module: str, key: str): """Initialize the configuration error. :param module: Name of the module, e.g., ``bioportal`` :param key: Name of the key inside the module, e.g., ``api_key`` """ self.module = module self.key = key def __str__(self) -> str: path = get_home().joinpath(self.module).with_suffix(".ini") return dedent( f"""\ Could not look up {self.module}/{self.key} and no default given. This can be solved with one of the following: 1. Set the {self.module.upper()}_{self.key.upper()} environment variable - Windows, via GUI: https://www.computerhope.com/issues/ch000549.htm - Windows, via CLI: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1 - Mac OS: https://apple.stackexchange.com/questions/106778/how-do-i-set-environment-variables-on-os-x - Linux: https://www.freecodecamp.org/news/how-to-set-an-environment-variable-in-linux/ 2. Use the PyStow CLI from the command line to set the configuration like so: $ pystow set {self.module} {self.key} <value> This creates an INI file in {path} with the configuration in the right place. 3. Create/edit an INI file in {path} and manually fill it in by 1) creating a section inside it called [{self.module}] and 2) setting a value for {self.key} = <value> that looks like: # {path} [{self.module}] {self.key} = <value> See https://github.com/cthoyt/pystow#%EF%B8%8F%EF%B8%8F-configuration for more information. """ )
def get_name() -> str: """Get the config home directory name. :returns: The name of the pystow home directory, either loaded from the :data:`CONFIG_NAME_ENVVAR`` environment variable or given by the default value :data:`CONFIG_NAME_DEFAULT`. """ return os.getenv(CONFIG_NAME_ENVVAR, default=CONFIG_NAME_DEFAULT) def get_home(ensure_exists: bool = True) -> Path: """Get the config home directory. :param ensure_exists: If true, ensures the directory is created :returns: A path object representing the pystow home directory, as one of: 1. :data:`CONFIG_HOME_ENVVAR` environment variable or 2. The default directory constructed in the user's home directory plus what's returned by :func:`get_name`. """ default = Path.home().joinpath(get_name()).expanduser() return getenv_path(CONFIG_HOME_ENVVAR, default, ensure_exists=ensure_exists) @lru_cache(maxsize=1) def _get_cfp(module: str) -> ConfigParser: cfp = ConfigParser() directory = get_home() # If a multi-part module was given like "zenodo:sandbox", # then only look for the first part "zenodo" as the file name if ":" in module: module = module.split(":", 1)[0] filenames = [ os.path.join(directory, "config.cfg"), os.path.join(directory, "config.ini"), os.path.join(directory, "pystow.cfg"), os.path.join(directory, "pystow.ini"), os.path.join(directory, f"{module}.cfg"), os.path.join(directory, f"{module}.ini"), os.path.join(directory, module, f"{module}.cfg"), os.path.join(directory, module, f"{module}.ini"), os.path.join(directory, module, "conf.ini"), os.path.join(directory, module, "config.ini"), os.path.join(directory, module, "conf.cfg"), os.path.join(directory, module, "config.cfg"), ] cfp.read(filenames) return cfp
[docs] def get_config( module: str, key: str, *, passthrough: X | None = None, default: X | None = None, dtype: type[X] | None = None, raise_on_missing: bool = False, ) -> Any: """Get a configuration value. :param module: Name of the module (e.g., ``pybel``) to get configuration for :param key: Name of the key (e.g., ``connection``) :param passthrough: If this is not none, will get returned :param default: If the environment and configuration files don't contain anything, this is returned. :param dtype: The datatype to parse out. Can either be :func:`int`, :func:`float`, :func:`bool`, or :func:`str`. If none, defaults to :func:`str`. :param raise_on_missing: If true, will raise a value error if no data is found and no default is given :returns: The config value or the default. :raises ConfigError: If ``raise_on_missing`` conditions are met """ if passthrough is not None: return _cast(passthrough, dtype) rv = os.getenv(f"{module.upper()}_{key.upper()}") if rv is not None: return _cast(rv, dtype) rv = _get_cfp(module).get(module, key, fallback=None) if rv is None: if default is None and raise_on_missing: raise ConfigError(module=module, key=key) return default return _cast(rv, dtype)
def _cast(rv: Any, dtype: None | Callable[..., Any]) -> Any: if not isinstance(rv, str): # if it's not a string, it doesn't need munging return rv if dtype in (None, str): # no munging necessary return rv if dtype in (int, float): return dtype(rv) if dtype is bool: if rv.lower() in ("t", "true", "yes", "1", 1, True): return True elif rv.lower() in ("f", "false", "no", "0", 0, False): return False else: raise ValueError(f"value can not be coerced into bool: {rv}") raise TypeError(f"dtype is invalid: {dtype}")
[docs] def write_config(module: str, key: str, value: str) -> None: """Write a configuration value. :param module: The name of the app (e.g., ``indra``) :param key: The key of the configuration in the app :param value: The value of the configuration in the app """ _get_cfp.cache_clear() cfp = ConfigParser() # If there's a multi-part module such as "zenodo:sandbox", # then write to zenodo.ini with section [zenodo:sandbox] fname = module.split(":", 1)[0] if ":" in module else module path = get_home().joinpath(fname).with_suffix(".ini") cfp.read(path) # If the file did not exist, then this section will be empty # and running set() would raise a configparser.NoSectionError. if not cfp.has_section(module): cfp.add_section(module) # Note that the section duplicates the file name cfp.set(section=module, option=key, value=value) with path.open("w") as file: cfp.write(file)