Source code for esmvalcore.config._config_object

"""Importable config object."""

from __future__ import annotations

import datetime
import os
import warnings
from pathlib import Path
from typing import TYPE_CHECKING

import dask.config

import esmvalcore
from esmvalcore.config._config_validators import (
    _deprecated_options_defaults,
    _deprecators,
    _validators,
)
from esmvalcore.config._validated_config import ValidatedConfig
from esmvalcore.exceptions import InvalidConfigParameter

if TYPE_CHECKING:
    from collections.abc import Iterable, Mapping

URL = (
    "https://docs.esmvaltool.org/projects/"
    "ESMValCore/en/latest/quickstart/configure.html"
)

# Configuration directory in which defaults are stored
DEFAULT_CONFIG_DIR = (
    Path(esmvalcore.__file__).parent / "config" / "configurations" / "defaults"
)


def _get_user_config_dir() -> Path:
    """Get user configuration directory."""
    if "ESMVALTOOL_CONFIG_DIR" in os.environ:
        user_config_dir = (
            Path(os.environ["ESMVALTOOL_CONFIG_DIR"]).expanduser().absolute()
        )
        if not user_config_dir.is_dir():
            msg = (
                f"Invalid configuration directory specified via "
                f"ESMVALTOOL_CONFIG_DIR environment variable: "
                f"{user_config_dir} is not an existing directory"
            )
            raise NotADirectoryError(msg)
        return user_config_dir
    return Path.home() / ".config" / "esmvaltool"


def _get_user_config_source() -> str:
    """Get source of user configuration directory."""
    if "ESMVALTOOL_CONFIG_DIR" in os.environ:
        return "ESMVALTOOL_CONFIG_DIR environment variable"
    return "default user configuration directory"


# User configuration directory
USER_CONFIG_DIR = _get_user_config_dir()

# Source of user configuration directory
USER_CONFIG_SOURCE = _get_user_config_source()


[docs] class Config(ValidatedConfig): """ESMValTool configuration object. Do not instantiate this class directly, but use :obj:`esmvalcore.config.CFG` instead. """ _validate = _validators _deprecate = _deprecators _deprecated_defaults = _deprecated_options_defaults _warn_if_missing = (("projects", URL),) def __init__(self, *args, **kwargs): """Initialize class instance.""" super().__init__(*args, **kwargs) msg = ( "Do not instantiate `Config` objects directly, this will lead " "to unexpected behavior. Use `esmvalcore.config.CFG` instead." ) warnings.warn(msg, UserWarning, stacklevel=2) @staticmethod def _get_config_dict_from_dirs(dirs: Iterable[str | Path]) -> dict: """Get configuration :obj:`dict` from directories.""" dirs_str: list[str] = [] for config_dir in dirs: abs_config_dir = Path(config_dir).expanduser().absolute() dirs_str.append(str(abs_config_dir)) return dask.config.collect(paths=dirs_str, env={})
[docs] def load_from_dirs(self, dirs: Iterable[str | Path]) -> None: """Clear and load configuration object from directories. This searches for all YAML files within the given directories and merges them together using :func:`dask.config.collect`. Nested objects are properly considered; see :func:`dask.config.update` for details. Values in the latter directories are preferred to those in the former. Options that are not explicitly specified via YAML files are set to the :ref:`default values <config_options>`. Note ---- Just like :func:`dask.config.collect`, this silently ignores non-existing directories. Parameters ---------- dirs: A list of directories to search for YAML configuration files. Raises ------ esmvalcore.exceptions.InvalidConfigParameter Invalid configuration option given. """ # Always consider default options; these have the lowest priority dirs = [DEFAULT_CONFIG_DIR, *list(dirs)] new_config_dict = self._get_config_dict_from_dirs(dirs) self.clear() self.update(new_config_dict) self.check_missing()
[docs] def reload(self) -> None: """Clear and reload the configuration object. This will read all YAML files in the user configuration directory (by default ``~/.config/esmvaltool``, but this can be changed with the ``ESMVALTOOL_CONFIG_DIR`` environment variable) and merges them together using :func:`dask.config.collect`. Nested objects are properly considered; see :func:`dask.config.update` for details. Options that are not explicitly specified via YAML files are set to the :ref:`default values <config_options>`. Note ---- If the user configuration directory does not exist, this will be silently ignored. Raises ------ esmvalcore.exceptions.InvalidConfigParameter Invalid configuration option given. """ try: self.load_from_dirs([USER_CONFIG_DIR]) except InvalidConfigParameter as exc: msg = ( f"Failed to parse configuration directory {USER_CONFIG_DIR} " f"({USER_CONFIG_SOURCE}): {exc!s}" ) raise InvalidConfigParameter(msg) from exc
[docs] def start_session(self, name: str) -> Session: """Start a new session from this configuration object. Parameters ---------- name: str Name of the session. Returns ------- Session """ with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="Do not instantiate `Session` objects directly", category=UserWarning, module="esmvalcore", ) return Session(config=self.copy(), name=name)
[docs] def update_from_dirs(self, dirs: Iterable[str | Path]) -> None: """Update configuration object from directories. This will first search for all YAML files within the given directories and merge them together using :func:`dask.config.collect` (if identical values are provided in multiple files, the value from the last file will be used). Then, the current configuration is merged with these new configuration options using :func:`dask.config.merge` (new values are preferred over old values). Nested objects are properly considered; see :func:`dask.config.update` for details. Note ---- Just like :func:`dask.config.collect`, this silently ignores non-existing directories. Parameters ---------- dirs: A list of directories to search for YAML configuration files. Raises ------ esmvalcore.exceptions.InvalidConfigParameter Invalid configuration option given. """ new_config_dict = self._get_config_dict_from_dirs(dirs) self.nested_update(new_config_dict)
[docs] def nested_update(self, new_options: Mapping) -> None: """Nested update of configuration object with another mapping. Merge the existing configuration object with a new mapping using :func:`dask.config.merge` (new values are preferred over old values). Nested objects are properly considered; see :func:`dask.config.update` for details. Parameters ---------- new_options: New configuration options. Raises ------ esmvalcore.exceptions.InvalidConfigParameter Invalid configuration option given. """ merged_config_dict = dask.config.merge(self, new_options) self.update(merged_config_dict) self.check_missing()
clear = ValidatedConfig.clear # to show this is in API doc context = ValidatedConfig.context # to show this is in API doc copy = ValidatedConfig.copy # to show this is in API doc
[docs] class Session(ValidatedConfig): """Container class for session configuration and directory information. Do not instantiate this class directly, but use :obj:`CFG.start_session` instead. Parameters ---------- config : dict Dictionary with configuration settings. name : str Name of the session to initialize, for example, the name of the recipe (default='session'). """ _validate = _validators _deprecate = _deprecators _deprecated_defaults = _deprecated_options_defaults relative_preproc_dir = Path("preproc") """Relative path to the preprocessor output directory, with respect to :attr:`session_dir`.""" relative_work_dir = Path("work") """Relative path to diagnostic script output directory, with respect to :attr:`session_dir`.""" relative_plot_dir = Path("plots") """Relative path to diagnostic script plot directory, with respect to :attr:`session_dir`.""" relative_run_dir = Path("run") """Relative path to the directory with information about the run, with respect to :attr:`session_dir`.""" relative_main_log = Path("run", "main_log.txt") """Relative path to the log file, with respect to :attr:`session_dir`.""" relative_main_log_debug = Path("run", "main_log_debug.txt") """Relative path to the debug log file, with respect to :attr:`session_dir`.""" relative_cmor_log = Path("run", "cmor_log.txt") """Relative path to the log file with CMOR check messages, with respect to :attr:`session_dir`.""" _relative_fixed_file_dir = Path("preproc", "fixed_files") def __init__(self, config: dict, name: str = "session") -> None: super().__init__(config) self.session_name: str | None = None self.set_session_name(name) msg = ( "Do not instantiate `Session` objects directly, this will lead " "to unexpected behavior. Use " "`esmvalcore.config.CFG.start_session` instead." ) warnings.warn(msg, UserWarning, stacklevel=2) def __repr__(self) -> str: return f"{self.__class__.__name__}(<config>, name={self.session_name})" def __str__(self) -> str: return repr(self)
[docs] def set_session_name(self, name: str = "session") -> None: """Set the name for the session. The `name` is used to name the session directory, e.g. `session_20201208_132800/`. The date is suffixed automatically. """ now = datetime.datetime.now(datetime.UTC).strftime("%Y%m%d_%H%M%S") self.session_name = f"{name}_{now}"
@property def session_dir(self): """Session directory. This is a uniquely named directory inside the :ref:`output directory <outputdata>`. """ return self["output_dir"] / self.session_name @property def preproc_dir(self): """Directory with preprocessor output files.""" return self.session_dir / self.relative_preproc_dir @property def work_dir(self): """Directory with diagnostic script output files.""" return self.session_dir / self.relative_work_dir @property def plot_dir(self): """Directory with diagnostic script plot files.""" return self.session_dir / self.relative_plot_dir @property def run_dir(self): """Directory containing information about the run.""" return self.session_dir / self.relative_run_dir @property def main_log(self): """Path to the log file.""" return self.session_dir / self.relative_main_log @property def main_log_debug(self): """Path to the debug log file.""" return self.session_dir / self.relative_main_log_debug @property def cmor_log(self): """Path to the log file with CMOR check messages.""" return self.session_dir / self.relative_cmor_log @property def _fixed_file_dir(self): """Return fixed file directory.""" return self.session_dir / self._relative_fixed_file_dir clear = ValidatedConfig.clear # to show this is in API doc context = ValidatedConfig.context # to show this is in API doc copy = ValidatedConfig.copy # to show this is in API doc
def _get_all_config_dirs(cli_config_dir: Path | None) -> list[Path]: """Get all configuration directories.""" config_dirs: list[Path] = [ DEFAULT_CONFIG_DIR, USER_CONFIG_DIR, ] if cli_config_dir is not None: config_dirs.append(cli_config_dir) return config_dirs def _get_all_config_sources(cli_config_dir: Path | None) -> list[str]: """Get all sources of configuration directories.""" config_sources: list[str] = [ "defaults", USER_CONFIG_SOURCE, ] if cli_config_dir is not None: config_sources.append("command line argument") return config_sources def _get_global_config() -> Config: """Get global configuration object.""" with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="Do not instantiate `Config` objects directly", category=UserWarning, module="esmvalcore", ) config_obj = Config() config_obj.reload() return config_obj # Initialize configuration objects CFG = _get_global_config()