Source code for esmvalcore.config._config_object
"""Importable config object."""
from __future__ import annotations
import os
import sys
from datetime import datetime
from pathlib import Path
from types import MappingProxyType
from typing import Optional
import yaml
import esmvalcore
from esmvalcore.cmor.check import CheckLevels
from esmvalcore.exceptions import InvalidConfigParameter
from ._config_validators import (
_deprecated_options_defaults,
_deprecators,
_validators,
)
from ._validated_config import ValidatedConfig
URL = ('https://docs.esmvaltool.org/projects/'
'ESMValCore/en/latest/quickstart/configure.html')
[docs]
class Config(ValidatedConfig):
"""ESMValTool configuration object.
Do not instantiate this class directly, but use
:obj:`esmvalcore.config.CFG` instead.
"""
_DEFAULT_USER_CONFIG_DIR = Path.home() / '.esmvaltool'
_validate = _validators
_deprecate = _deprecators
_deprecated_defaults = _deprecated_options_defaults
_warn_if_missing = (
('drs', URL),
('rootpath', URL),
)
@classmethod
def _load_user_config(
cls,
filename: Optional[os.PathLike | str] = None,
raise_exception: bool = True,
):
"""Load user configuration from the given file.
The config is cleared and updated in-place.
Parameters
----------
filename:
Name of the user configuration file (must be YAML format). If
`None`, use the rules given in `Config._get_config_user_path` to
determine the path.
raise_exception : bool
If ``True``, raise an exception if `filename` cannot be found. If
``False``, silently pass and use the default configuration. This
setting is necessary during the loading of this module when no
configuration file is given (relevant if used within a script or
notebook).
"""
new = cls()
new.update(CFG_DEFAULT)
config_user_path = cls._get_config_user_path(filename)
try:
mapping = cls._read_config_file(config_user_path)
mapping['config_file'] = config_user_path
except FileNotFoundError:
if raise_exception:
raise
mapping = {}
try:
new.update(mapping)
new.check_missing()
except InvalidConfigParameter as exc:
raise InvalidConfigParameter(
f"Failed to parse user configuration file {config_user_path}: "
f"{str(exc)}"
) from exc
return new
@classmethod
def _load_default_config(cls):
"""Load the default configuration."""
new = cls()
package_config_user_path = Path(
esmvalcore.__file__
).parent / 'config-user.yml'
mapping = cls._read_config_file(package_config_user_path)
# Add defaults that are not available in esmvalcore/config-user.yml
mapping['check_level'] = CheckLevels.DEFAULT
mapping['config_file'] = package_config_user_path
mapping['diagnostics'] = None
mapping['extra_facets_dir'] = tuple()
mapping['max_datasets'] = None
mapping['max_years'] = None
mapping['resume_from'] = []
mapping['run_diagnostic'] = True
mapping['skip_nonexistent'] = False
new.update(mapping)
return new
@staticmethod
def _read_config_file(config_user_path: Path) -> dict:
"""Read configuration file and store settings in a dictionary."""
if not config_user_path.is_file():
raise FileNotFoundError(
f"Config file '{config_user_path}' does not exist"
)
with open(config_user_path, 'r', encoding='utf-8') as file:
cfg = yaml.safe_load(file)
return cfg
@staticmethod
def _get_config_user_path(
filename: Optional[os.PathLike | str] = None
) -> Path:
"""Get path to user configuration file.
`filename` can be given as absolute or relative path. In the latter
case, search in the current working directory and `~/.esmvaltool` (in
that order).
If `filename` is not given, try to get user configuration file from the
following locations (sorted by descending priority):
1. Internal `_ESMVALTOOL_USER_CONFIG_FILE_` environment variable
(this ensures that any subprocess spawned by the esmvaltool program
will use the correct user configuration file).
2. Command line arguments `--config-file` or `--config_file` (both
variants are allowed by the fire module), but only if script name is
`esmvaltool`.
3. `config-user.yml` within default ESMValTool configuration directory
`~/.esmvaltool`.
Note
----
This will NOT check if the returned file actually exists to allow
loading the module without any configuration file (this is relevant if
the module is used within a script or notebook). To check if the file
actually exists, use the method `load_from_file` (this is done when
using the `esmvaltool` CLI).
If used within the esmvaltool program, set the
_ESMVALTOOL_USER_CONFIG_FILE_ at the end of this method to make sure
that subsequent calls of this method (also in suprocesses) use the
correct user configuration file.
"""
# (1) Try to get user configuration file from `filename` argument
config_user = filename
# (2) Try to get user configuration file from internal
# _ESMVALTOOL_USER_CONFIG_FILE_ environment variable
if (
config_user is None and
'_ESMVALTOOL_USER_CONFIG_FILE_' in os.environ
):
config_user = os.environ['_ESMVALTOOL_USER_CONFIG_FILE_']
# (3) Try to get user configuration file from CLI arguments
if config_user is None:
config_user = Config._get_config_path_from_cli()
# (4) Default location
if config_user is None:
config_user = Config._DEFAULT_USER_CONFIG_DIR / 'config-user.yml'
config_user = Path(config_user).expanduser()
# Also search path relative to ~/.esmvaltool if necessary
if not (config_user.is_file() or config_user.is_absolute()):
config_user = Config._DEFAULT_USER_CONFIG_DIR / config_user
config_user = config_user.absolute()
# If used within the esmvaltool program, make sure that subsequent
# calls of this method (also in suprocesses) use the correct user
# configuration file
if Path(sys.argv[0]).name == 'esmvaltool':
os.environ['_ESMVALTOOL_USER_CONFIG_FILE_'] = str(config_user)
return config_user
@staticmethod
def _get_config_path_from_cli() -> None | str:
"""Try to get configuration path from CLI arguments.
The hack of directly parsing the CLI arguments here (instead of using
the fire or argparser module) ensures that the correct user
configuration file is used. This will always work, regardless of when
this module has been imported in the code.
Note
----
This only works if the script name is `esmvaltool`. Does not check if
file exists.
"""
if Path(sys.argv[0]).name != 'esmvaltool':
return None
for arg in sys.argv:
for opt in ('--config-file', '--config_file'):
if opt in arg:
# Parse '--config-file=/file.yml' or
# '--config_file=/file.yml'
partition = arg.partition('=')
if partition[2]:
return partition[2]
# Parse '--config-file /file.yml' or
# '--config_file /file.yml'
config_idx = sys.argv.index(opt)
if config_idx == len(sys.argv) - 1: # no file given
return None
return sys.argv[config_idx + 1]
return None
[docs]
def load_from_file(
self,
filename: Optional[os.PathLike | str] = None,
) -> None:
"""Load user configuration from the given file."""
self.clear()
self.update(Config._load_user_config(filename))
[docs]
def reload(self):
"""Reload the config file."""
if 'config_file' not in self:
raise ValueError(
"Cannot reload configuration, option 'config_file' is "
"missing; make sure to only use the `CFG` object from the "
"`esmvalcore.config` module"
)
self.load_from_file(self['config_file'])
[docs]
def start_session(self, name: str):
"""Start a new session from this configuration object.
Parameters
----------
name: str
Name of the session.
Returns
-------
Session
"""
return Session(config=self.copy(), name=name)
[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_work_dir = Path('work')
relative_plot_dir = Path('plots')
relative_run_dir = Path('run')
relative_main_log = Path('run', 'main_log.txt')
relative_main_log_debug = Path('run', 'main_log_debug.txt')
_relative_fixed_file_dir = Path('preproc', 'fixed_files')
def __init__(self, config: dict, name: str = 'session'):
super().__init__(config)
self.session_name: str | None = None
self.set_session_name(name)
[docs]
def set_session_name(self, name: str = 'session'):
"""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.utcnow().strftime("%Y%m%d_%H%M%S")
self.session_name = f"{name}_{now}"
@property
def session_dir(self):
"""Return session directory."""
return self['output_dir'] / self.session_name
@property
def preproc_dir(self):
"""Return preproc directory."""
return self.session_dir / self.relative_preproc_dir
@property
def work_dir(self):
"""Return work directory."""
return self.session_dir / self.relative_work_dir
@property
def plot_dir(self):
"""Return plot directory."""
return self.session_dir / self.relative_plot_dir
@property
def run_dir(self):
"""Return run directory."""
return self.session_dir / self.relative_run_dir
@property
def config_dir(self):
"""Return user config directory."""
return Path(self['config_file']).parent
@property
def main_log(self):
"""Return main log file."""
return self.session_dir / self.relative_main_log
@property
def main_log_debug(self):
"""Return main log debug file."""
return self.session_dir / self.relative_main_log_debug
@property
def _fixed_file_dir(self):
"""Return fixed file directory."""
return self.session_dir / self._relative_fixed_file_dir
# Initialize configuration objects
CFG_DEFAULT = MappingProxyType(Config._load_default_config())
CFG = Config._load_user_config(raise_exception=False)