From c121513650831fdae6dcf32f524efd2f3cc6f3de Mon Sep 17 00:00:00 2001 From: Maximilian Linhoff Date: Fri, 8 Nov 2024 15:30:14 +0100 Subject: [PATCH] Adapt reporting of depdency versions in info tool --- src/ctapipe/core/provenance.py | 56 ++++++++++++++++++++--- src/ctapipe/core/tests/test_provenance.py | 8 ++++ src/ctapipe/tools/info.py | 50 ++++++++++---------- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/ctapipe/core/provenance.py b/src/ctapipe/core/provenance.py index a8f602e127f..e827637f382 100644 --- a/src/ctapipe/core/provenance.py +++ b/src/ctapipe/core/provenance.py @@ -14,10 +14,12 @@ import warnings from collections import UserList from contextlib import contextmanager +from functools import cache from importlib import import_module -from importlib.metadata import distributions, version +from importlib.metadata import Distribution, distributions from os.path import abspath from pathlib import Path +from types import ModuleType import psutil from astropy.time import Time @@ -45,17 +47,59 @@ ] +@cache +def modules_of_distribution(distribution: Distribution): + modules = distribution.read_text("top_level.txt") + if modules is None: + return None + return set(modules.splitlines()) + + +@cache +def get_distribution_of_module(module: ModuleType | str): + """Get the package distribution for an imported module""" + if isinstance(module, str): + name = module + module = import_module(module) + else: + name = module.__name__ + + path = Path(module.__file__).absolute() + + for dist in distributions(): + modules = modules_of_distribution(dist) + if modules is None: + base = dist.locate_file("") + if dist.files is not None and any(path == base / f for f in dist.files): + return dist + elif name in modules: + return dist + + raise ValueError(f"Could not find a distribution for module: {module}") + + def get_module_version(name): + """ + Get the version of a python *module*, something you can import. + + If the module does not expose a ``__version__`` attribute, this function + will try to determine the *distribution* of the module and return its + version. + """ + # we try first with module.__version__ + # to support editable installs try: module = import_module(name) + except ModuleNotFoundError: + return "not installed" + + try: return module.__version__ except AttributeError: try: - return version(name) + return get_distribution_of_module(module).version except Exception: return "unknown" - except ImportError: - return "not installed" class MissingReferenceMetadata(UserWarning): @@ -351,15 +395,13 @@ def _sortkey(dist): def _get_system_provenance(): - """return JSON string containing provenance for all things that are + """return a dict containing provenance for all things that are fixed during the runtime""" bits, linkage = platform.architecture() return dict( ctapipe_version=__version__, - ctapipe_resources_version=get_module_version("ctapipe_resources"), - eventio_version=get_module_version("eventio"), ctapipe_svc_path=os.getenv("CTAPIPE_SVC_PATH"), executable=sys.executable, platform=dict( diff --git a/src/ctapipe/core/tests/test_provenance.py b/src/ctapipe/core/tests/test_provenance.py index 465e11ba27e..2ef0c7cb896 100644 --- a/src/ctapipe/core/tests/test_provenance.py +++ b/src/ctapipe/core/tests/test_provenance.py @@ -76,3 +76,11 @@ def test_provenance_input_reference_meta(provenance: Provenance, dl1_file): assert "reference_meta" in input_meta assert "CTA PRODUCT ID" in input_meta["reference_meta"] Reference.from_dict(input_meta["reference_meta"]) + + +def test_get_distribution_of_module(): + from ctapipe.core.provenance import get_distribution_of_module + + assert get_distribution_of_module("yaml").name == "PyYAML" + assert get_distribution_of_module("sklearn").name == "scikit-learn" + assert get_distribution_of_module("ctapipe_test_plugin").version == "0.1.0" diff --git a/src/ctapipe/tools/info.py b/src/ctapipe/tools/info.py index f100b24e80e..70aee3529ab 100644 --- a/src/ctapipe/tools/info.py +++ b/src/ctapipe/tools/info.py @@ -3,8 +3,12 @@ import logging import os import sys +from importlib.metadata import PackageNotFoundError, requires, version from importlib.resources import files +from packaging.markers import default_environment +from packaging.requirements import Requirement + from ..core import Provenance, get_module_version from ..core.plugins import detect_and_import_plugins from ..utils import datasets @@ -12,27 +16,6 @@ __all__ = ["info"] -# TODO: this list should be global (or generated at install time) -_dependencies = sorted( - [ - "astropy", - "matplotlib", - "numpy", - "traitlets", - "sklearn", - "scipy", - "numba", - "pytest", - "iminuit", - "tables", - "eventio", - ] -) - -_optional_dependencies = sorted( - ["ctapipe_resources", "pytest", "graphviz", "matplotlib"] -) - def main(args=None): parser = get_parser(info) @@ -162,15 +145,28 @@ def _info_dependencies(): """Print info about dependencies.""" print("\n*** ctapipe core dependencies ***\n") - for name in _dependencies: - version = get_module_version(name) - print(f"{name:>20s} -- {version}") + env = default_environment() + requirements = [Requirement(r) for r in requires("ctapipe")] + dependencies = [ + r.name for r in requirements if r.marker is None or r.marker.evaluate(env) + ] + + env["extra"] = "all" + optional_dependencies = [ + r.name for r in requirements if r.marker is not None and r.marker.evaluate(env) + ] + + for name in dependencies: + print(f"{name:>20s} -- {version(name)}") print("\n*** ctapipe optional dependencies ***\n") - for name in _optional_dependencies: - version = get_module_version(name) - print(f"{name:>20s} -- {version}") + for name in optional_dependencies: + try: + v = version(name) + except PackageNotFoundError: + v = "not installed" + print(f"{name:>20s} -- {v}") def _info_resources():