"""
Module to deal with required and optional dependencies.
Optional dependencies are defined in pyproject.toml.
In any module that requires an optional dependency the import should be
conditioned:
if HAS_DEPENDENCY["package"]: import package
Any function that makes use of the optional dependency should be decorated with
@needs_package("package")
CONSTANTS
.. autosummary::
:toctree: ./
INSTALL_REQUIRES
EXTRAS_REQUIRE
IMPORT_NAMES
HAS_DEPENDENCY
"""
from __future__ import annotations
import importlib.metadata
import importlib.util
import logging
import os
import re
import sys
from collections.abc import Callable, Iterable
from enum import Enum
from functools import wraps
from typing import Any, ParamSpec, TypeVar
logger = logging.getLogger(__name__)
__all__: list[str] = [
"needs_package_version",
"needs_package",
"IMPORT_NAMES",
"INSTALL_REQUIRES",
"EXTRAS_REQUIRE",
"HAS_DEPENDENCY",
"QtBindings",
]
F = TypeVar("F", bound=Callable[..., Any])
P = ParamSpec("P")
T = TypeVar("T")
def _has_dependency_factory(
packages: Iterable[str], import_names: dict[str, str] | None = None
) -> dict[str, bool]:
if import_names is None:
import_names = IMPORT_NAMES
has_dependency = dict()
for package in packages:
key = import_names.get(package, package)
value = importlib.util.find_spec(key) is not None
has_dependency[key] = value
return has_dependency
[docs]
def needs_package(
package: str,
import_names: dict[str, str] | None = None,
has_dependency: dict[str, bool] | None = None,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""
Function that returns a decorator to check for optional dependency.
Parameters
----------
package
Package or dependency name that needs to be imported.
import_names
Mapping of package names onto import names.
has_dependency
Dictionary with bool indicator if package (import name) is available.
Returns
-------
callable
A decorator that raises ImportError if package is not available.
"""
if import_names is None:
import_names = IMPORT_NAMES
if has_dependency is None:
has_dependency = HAS_DEPENDENCY
import_name = import_names.get(package, package)
def decorator(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
if not has_dependency[import_name]: # type: ignore
raise ImportError(
f"Function {func} needs {import_name} which cannot be imported."
)
return func(*args, **kwargs)
return wrapper
return decorator
def _get_dependencies(package: str) -> tuple[set[str], set[str]]:
"""
Read out all required and optional (extra) dependencies for package.
Parameters
----------
package
Package or dependency name to be checked.
Returns
-------
tuple[set[str], set[str]]
PyPI names
"""
requires = importlib.metadata.requires(package)
if requires is None:
required_dependencies: set[str] = set()
extra_dependencies: set[str] = set()
else:
pattern = r"[\w-]+"
required_dependencies = set()
extra_dependencies = set()
for item in requires:
match = re.match(pattern, item)
if match is None:
pass
else:
if "extra ==" in item:
extra_dependencies.add(match.group())
else:
required_dependencies.add(match.group())
return required_dependencies, extra_dependencies
#: List of required dependencies (PyPi package names)
INSTALL_REQUIRES: set[str] = set()
#: List of optional dependencies (PyPi package names)
EXTRAS_REQUIRE: set[str] = set()
INSTALL_REQUIRES, EXTRAS_REQUIRE = _get_dependencies(package="locan")
#: A dictionary mapping PyPi package names to import names if they are different
IMPORT_NAMES = dict()
IMPORT_NAMES["ruamel"] = "ruamel.yaml"
IMPORT_NAMES["fast-histogram"] = "fast_histogram"
IMPORT_NAMES["boost-histogram"] = "boost_histogram"
IMPORT_NAMES["protobuf"] = "google.protobuf"
IMPORT_NAMES["scikit-image"] = "skimage"
IMPORT_NAMES["scikit-learn"] = "sklearn"
IMPORT_NAMES["pytest-qt"] = "pytestqt"
IMPORT_NAMES["mpl-scatter-density"] = "mpl_scatter_density"
#: A dictionary indicating if dependency is available.
HAS_DEPENDENCY = _has_dependency_factory(
packages=INSTALL_REQUIRES.union(EXTRAS_REQUIRE)
)
# Possible python bindings to interact with QT
[docs]
class QtBindings(Enum):
"""
Python bindings to interact with Qt.
"""
NONE = ""
PYSIDE2 = "pyside2"
PYQT5 = "pyqt5"
PYSIDE6 = "pyside6"
PYQT6 = "pyqt6"
def _set_qt_binding(qt_binding: QtBindings | str) -> str:
"""
Check if qtpy can import `qt_binding` and return the qt_binding that will be used.
Checks os.environ["QT_API"] first.
If os.environ["QT_API"] is set it will take precedence over `qt_bindings`.
Note
-----
This function must be used before qtpy is imported for the first time
to be effective.
Returns
-------
str
qt-bindings or empty string
"""
if isinstance(qt_binding, QtBindings):
qt_api = qt_binding.value
elif qt_binding:
qt_api = qt_binding.lower()
else:
qt_api = ""
if "qtpy" in sys.modules:
logger.warning("qtpy has already been loaded. QT_BINDING cannot be changed.")
elif os.environ.get("QT_API", ""):
qt_api = os.environ["QT_API"]
else:
if qt_api:
os.environ["QT_API"] = qt_api
try:
from qtpy import ( # noqa: F401 # import API alone does not raise ModuleNotFoundError if no module is available
API,
QtCore,
)
if qt_api and qt_api != API:
logger.warning(
f"QT_BINDING {qt_api} is not available - {API} is used instead."
)
os.environ["QT_API"] = API
qt_api = API
except ImportError:
if qt_api:
logger.warning("QT_BINDING is not available.")
qt_api = ""
return qt_api
[docs]
def needs_package_version(
package: str,
major: str,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""
Function that returns a decorator to check for package.version starting with major.
Parameters
----------
package
Package or dependency name to be checked.
major
Major version number that is required.
Returns
-------
callable
A decorator that raises RuntimeError if package is not available.
"""
def decorator(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
version = importlib.metadata.version(package)
if not version.startswith(major): # type: ignore
raise RuntimeError(
f"Function {func} needs {package}~{major}. "
f"The installed version is {package}={version}."
)
return func(*args, **kwargs)
return wrapper
return decorator