"""Configuration tools."""
# NOTE: this module gets imported by `finesse` directly, so cannot itself import from
# `finesse` and cannot import packages that themselves import from `finesse`.
import importlib.resources
import logging
import os
from configparser import RawConfigParser
from pathlib import Path
from . import datastore
from .utilities import option_list
_PACKAGE_LOGGER = logging.getLogger(__package__)
show_progress_bars = False
[docs]def config_instance():
"""The Finesse configuration object for the current session.
Returns
-------
:class:`configparser.RawConfigParser`
The Finesse configuration object.
"""
return datastore.init_singleton(_FinesseConfig)
class _FinesseConfig(RawConfigParser):
"""The built-in and user configuration for Finesse.
Do not instantiate this class directly; use :func:`config_instance`.
"""
# Order in which user configs are loaded.
_USER_CONFIG_LOAD_ORDER = ["user_config_path", "cwd_config_path"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.write_user_config()
self._load_finesse_configs()
@classmethod
def user_config_paths(cls):
return {
config: getattr(cls, config)() for config in cls._USER_CONFIG_LOAD_ORDER
}
@classmethod
def user_config_path(cls):
return cls.user_config_dir() / "usr.ini"
@classmethod
def cwd_config_path(cls):
return Path.cwd() / "finesse.ini"
@classmethod
def user_config_dir(cls):
r"""The path to the user's config directory for Finesse.
The exact path is determined by the current platform and the presence of certain
environment variables:
.. rubric:: Windows
A folder called ``finesse`` in the folder pointed to by the environment variable
``%APPDATA`` (usually ``%HOMEPATH%\AppData\Roaming``).
.. rubric:: POSIX (including macOS and WSL)
A directory called ``finesse`` inside either the path pointed to by the
environment variable ``XDG_CONFIG_HOME`` or, if that value cannot be found or is
empty, ``~/.config``.
Returns
-------
:py:class:`pathlib.Path` or None
The path to the Finesse config directory.
Raises
------
:py:class:`RuntimeError`
If no config directory can be determined.
"""
from .env import IS_WINDOWS
if IS_WINDOWS:
try:
config_dir = Path(os.environ["APPDATA"])
except KeyError:
# NOTE: we assume %APPDATA% always exists on any normal Windows machine,
# which should be the case. If it's not, we might need to change Finesse
# to handle having no user config path.
raise RuntimeError(
r"The %APPDATA% environment variable is required for Finesse to "
r"store user configuration, but it was not found. Please ensure "
r"this environment variable exists in the environment in which "
r"Finesse is being run."
)
else:
# Path.home() raises RuntimeError if no home is found.
config_dir = Path(
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
)
return config_dir / "finesse"
@classmethod
def write_user_config(cls, force=False):
"""Copy the default config files to the user's config directory."""
logger = logging.getLogger(__name__)
user_config_path = cls.user_config_path()
if force or not user_config_path.is_file():
# Copy barebone user config file contents into user's user config path.
logger.info(f"Writing user config file to {user_config_path}.")
user_config_path.parent.mkdir(parents=True, exist_ok=True)
with user_config_path.open("wb") as fobj:
fobj.write(
importlib.resources.files(__package__)
.joinpath("usr.ini.dist")
.read_bytes()
)
def _load_finesse_configs(self):
"""Read the built-in and any user configuration files.
The built-in configuration is loaded first, then the user configuration files
are loaded in the order specified in :attr:`.USER_PATHS`. This means user
configuration options can overwrite built-in options, and options from later
paths in :attr:`.USER_PATHS` can overwrite options from earlier paths.
"""
user_config_paths = self.user_config_paths().values()
# Load the bundled configuration.
self.read_string(
(importlib.resources.files("finesse") / "finesse.ini").read_text(),
source="<bundled finesse.ini>",
)
# Parse all configurations, from lowest to highest priority.
parsed = self.read(user_config_paths)
if not parsed:
paths = option_list(user_config_paths)
raise ConfigNotFoundError(f"Could not find user config files at {paths}.")
[docs]class ConfigNotFoundError(Exception):
"""Indicates a Finesse configuration could not be loaded."""