Source code for abtem.core.config

from __future__ import annotations

import os
import threading
import warnings
from collections.abc import Mapping, Sequence
from typing import Any, Literal, Union

import yaml  # type: ignore
from dask.config import canonical_name, collect, update

no_default = "__no_default__"

if "ABTEM_CONFIG" in os.environ:
    PATH = os.environ["ABTEM_CONFIG"]
else:
    PATH = os.path.join(os.path.expanduser("~"), ".config", "abtem")

config: dict = {}

config_lock = threading.Lock()

defaults: list[Mapping] = []


[docs] class set: """Temporarily set configuration values within a context manager Parameters ---------- arg : mapping or None, optional A mapping of configuration key-value pairs to set. **kwargs : Additional key-value pairs to set. If ``arg`` is provided, values set in ``arg`` will be applied before those in ``kwargs``. Double-underscores (``__``) in keyword arguments will be replaced with ``.``, allowing nested values to be easily set. """ config: dict # [(op, path, value), ...] _record: list[tuple[Literal["insert", "replace"], tuple[str, ...], Any]] def __init__( self, arg: Union[Mapping, None] = None, config: dict = config, lock: threading.Lock = config_lock, **kwargs, ): with lock: self.config = config self._record = [] if arg is not None: for key, value in arg.items(): key = check_deprecations(key) self._assign(key.split("."), value, config) if kwargs: for key, value in kwargs.items(): key = key.replace("__", ".") key = check_deprecations(key) self._assign(key.split("."), value, config) def __enter__(self): return self.config def __exit__(self, type, value, traceback): for op, path, value in reversed(self._record): d = self.config if op == "replace": for key in path[:-1]: d = d.setdefault(key, {}) d[path[-1]] = value else: # insert for key in path[:-1]: try: d = d[key] except KeyError: break else: d.pop(path[-1], None) def _assign( self, keys: Sequence[str], value: Any, d: dict, path: tuple[str, ...] = (), record: bool = True, ) -> None: """Assign value into a nested configuration dictionary Parameters ---------- keys : Sequence[str] The nested path of keys to assign the value. value : object d : dict The part of the nested dictionary into which we want to assign the value path : tuple[str], optional The path history up to this point. record : bool, optional Whether this operation needs to be recorded to allow for rollback. """ key = canonical_name(keys[0], d) path = path + (key,) if len(keys) == 1: if record: if key in d: self._record.append(("replace", path, d[key])) else: self._record.append(("insert", path, None)) d[key] = value else: if key not in d: if record: self._record.append(("insert", path, None)) d[key] = {} # No need to record subsequent operations after an insert record = False self._assign(keys[1:], value, d[key], path, record=record)
[docs] def refresh( config: dict = config, defaults: list[Mapping] = defaults, **kwargs ) -> None: """ Update configuration by re-reading yaml files and env variables This mutates the global abtem.config.config, or the config parameter if passed in. This goes through the following stages: 1. Clearing out all old configuration 2. Updating from the stored defaults from downstream libraries (see update_defaults) 3. Updating from yaml files and environment variables Note that some functionality only checks configuration once at startup and may not change behavior, even if configuration changes. It is recommended to restart your python process if convenient to ensure that new configuration changes take place. See Also -------- abtem.config.collect: for parameters abtem.config.update_defaults """ config.clear() for d in defaults: update(config, d, priority="old") update(config, collect(**kwargs))
[docs] def get( key: str, default: Any = no_default, config: dict = config, override_with: Any = None, ) -> Any: """ Get elements from global config If ``override_with`` is not None this value will be passed straight back. Useful for getting kwarg defaults from abtek config. Use '.' for nested access """ if override_with is not None: return override_with keys = key.split(".") result = config for k in keys: k = canonical_name(k, result) try: result = result[k] except (TypeError, IndexError, KeyError): if default is not no_default: return default else: raise return result
[docs] def update_defaults( new: Mapping, config: dict = config, defaults: list[Mapping] = defaults ) -> None: """Add a new set of defaults to the configuration It does two things: 1. Add the defaults to a global collection to be used by refresh later 2. Updates the global config with the new configuration prioritizing older values over newer ones """ defaults.append(new) update(config, new, priority="old")
deprecations: dict[str, str | None] = {}
[docs] def check_deprecations(key: str, deprecations: dict = deprecations) -> str: """Check if the provided value has been renamed or removed Parameters ---------- key : str The configuration key to check deprecations : Dict[str, str] The mapping of aliases Returns ------- new: str The proper key, whether the original (if no deprecation) or the aliased value """ if key in deprecations: new = deprecations[key] if new: warnings.warn( 'Configuration key "{}" has been deprecated. ' 'Please use "{}" instead'.format(key, new) ) return new else: raise ValueError(f'Configuration value "{key}" has been removed') else: return key
def _initialize() -> None: fn = os.path.join(os.path.dirname(__file__), "abtem.yaml") with open(fn, encoding="utf-8") as f: _defaults = yaml.safe_load(f) update_defaults(_defaults) refresh() _initialize()