Source code for pde.tools.config

"""
Handles configuration variables of the package

.. autosummary::
   :nosignatures:

   Config
   get_package_versions
   parse_version_str
   check_package_version
   environment
   
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
"""

import collections
import importlib
import sys
import warnings
from typing import Any, Dict, List

from .misc import module_available
from .parameters import Parameter

# define default parameter values
DEFAULT_CONFIG: List[Parameter] = [
    Parameter(
        "numba.debug",
        False,
        bool,
        "Determines whether numba used the debug mode for compilation. If enabled, "
        "this emits extra information that might be useful for debugging.",
    ),
    Parameter(
        "numba.fastmath",
        True,
        bool,
        "Determines whether the fastmath flag is set during compilation. This affects "
        "the precision of the mathematical calculations.",
    ),
    Parameter(
        "numba.parallel",
        True,
        bool,
        "Determines whether multiple cores are used in numba-compiled code.",
    ),
    Parameter(
        "numba.parallel_threshold",
        256**2,
        int,
        "Minimal number of support points before multithreading or multiprocessing is "
        "enabled in the numba compilations.",
    ),
]


[docs]class Config(collections.UserDict): """class handling the package configuration""" def __init__(self, items: Dict[str, Any] = None, mode: str = "update"): """ Args: items (dict, optional): Configuration values that should be added or overwritten to initialize the configuration. mode (str): Defines the mode in which the configuration is used. Possible values are * `insert`: any new configuration key can be inserted * `update`: only the values of pre-existing items can be updated * `locked`: no values can be changed Note that the items specified by `items` will always be inserted, independent of the `mode`. """ self.mode = "insert" # temporarily allow inserting items super().__init__({p.name: p for p in DEFAULT_CONFIG}) if items: self.update(items) self.mode = mode def __getitem__(self, key: str): """retrieve item `key`""" parameter = self.data[key] if isinstance(parameter, Parameter): return parameter.convert() else: return parameter def __setitem__(self, key: str, value): """update item `key` with `value`""" if self.mode == "insert": self.data[key] = value elif self.mode == "update": try: self[key] # test whether the key already exist (including magic keys) except KeyError: raise KeyError( f"{key} is not present and config is not in `insert` mode" ) self.data[key] = value elif self.mode == "locked": raise RuntimeError("Configuration is locked") else: raise ValueError(f"Unsupported configuration mode `{self.mode}`") def __delitem__(self, key: str): """removes item `key`""" if self.mode == "insert": del self.data[key] else: raise RuntimeError("Configuration is not in `insert` mode")
[docs] def to_dict(self) -> Dict[str, Any]: """convert the configuration to a simple dictionary Returns: dict: A representation of the configuration in a normal :class:`dict`. """ return {k: v for k, v in self.items()}
def __repr__(self) -> str: """represent the configuration as a string""" return f"{self.__class__.__name__}({repr(self.to_dict())})"
[docs]def get_package_versions( packages: List[str], *, na_str="not available" ) -> Dict[str, str]: """tries to load certain python packages and returns their version Args: packages (list): The names of all packages na_str (str): Text to return if package is not available Returns: dict: Dictionary with version for each package name """ versions: Dict[str, str] = {} for name in sorted(packages): try: module = importlib.import_module(name) except ImportError: versions[name] = na_str else: versions[name] = module.__version__ return versions
[docs]def parse_version_str(ver_str: str) -> List[int]: """helper function converting a version string into a list of integers""" result = [] for token in ver_str.split(".")[:3]: try: result.append(int(token)) except ValueError: pass return result
[docs]def check_package_version(package_name: str, min_version: str): """checks whether a package has a sufficient version""" msg = f"`{package_name}` version {min_version} required for py-pde" try: # obtain version of the package version = importlib.import_module(package_name).__version__ except ImportError: warnings.warn(f"{msg} (but none installed)") else: # check whether it is installed and works if parse_version_str(version) < parse_version_str(min_version): warnings.warn(f"{msg} (installed: {version})")
[docs]def environment() -> Dict[str, Any]: """obtain information about the compute environment Returns: dict: information about the python installation and packages """ import matplotlib as mpl from .. import __version__ as package_version from .. import config from .numba import numba_environment from .plotting import get_plotting_context result: Dict[str, Any] = {} result["package version"] = package_version result["python version"] = sys.version result["platform"] = sys.platform # add the package configuration result["config"] = config.to_dict() # add details for mandatory packages result["mandatory packages"] = get_package_versions( ["matplotlib", "numba", "numpy", "scipy", "sympy"] ) result["matplotlib environment"] = { "backend": mpl.get_backend(), "plotting context": get_plotting_context().__class__.__name__, } # add details about optional packages result["optional packages"] = get_package_versions( ["h5py", "napari", "pandas", "pyfftw", "tqdm"] ) if module_available("numba"): result["numba environment"] = numba_environment() return result