Coverage for src / mafw / devtools / __init__.py: 100%
44 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-28 13:34 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-28 13:34 +0000
1# Copyright 2026 European Union
2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
3# SPDX-License-Identifier: EUPL-1.2
4"""
5MAFw development tools package.
7This package contains the business logic modules used by the ``release-mgt``
8and ``multiversion-doc`` CLI scripts. It requires optional development
9dependencies (``requests``, ``packaging``) that are not part of the core
10MAFw runtime.
12Use :func:`ensure_devtools_available` as a guard before importing subpackages.
13"""
15from __future__ import annotations
18class DevtoolsError(Exception):
19 """Base exception for MAFw development tool errors.
21 This exception replaces direct usage of :exc:`click.ClickException` in
22 business-logic modules so that the library layer remains independent of the
23 CLI framework.
24 """
27_devtools_checked: bool = False
28"""Module-level flag to skip repeated import checks after the first successful call."""
31def _read_devtools_deps() -> list[str]:
32 """Read the ``devtools`` optional-dependency names from MAFw package metadata.
34 This function requires ``packaging`` to be importable (the caller ensures
35 this). It uses :class:`~packaging.requirements.Requirement` to parse the
36 dependency specifiers reliably.
38 Falls back to reading ``pyproject.toml`` from the source tree if the
39 installed metadata does not yet include the ``devtools`` extra (e.g. stale
40 editable install).
42 :return: List of top-level package names required by the ``devtools`` extra.
43 :rtype: list[str]
44 """
45 import importlib.metadata
47 from packaging.requirements import Requirement
49 try:
50 requires = importlib.metadata.metadata('mafw').get_all('Requires-Dist') or []
51 names: list[str] = []
52 for entry in requires:
53 req = Requirement(entry)
54 # Only match entries whose marker explicitly mentions extra == "devtools".
55 if req.marker and 'extra == "devtools"' in str(req.marker):
56 names.append(req.name.replace('-', '_').lower())
57 if names:
58 return names
59 except importlib.metadata.PackageNotFoundError:
60 pass
62 # Fallback: read pyproject.toml from the source tree.
63 import tomllib
64 from pathlib import Path
66 pyproject = Path(__file__).resolve().parents[2] / 'pyproject.toml'
67 if pyproject.exists():
68 with pyproject.open('rb') as f:
69 data = tomllib.load(f)
70 deps = data.get('project', {}).get('optional-dependencies', {}).get('devtools', [])
71 return [Requirement(dep).name.replace('-', '_').lower() for dep in deps]
73 return []
76def ensure_devtools_available() -> None:
77 """Verify that optional development dependencies are importable.
79 The list of required packages is read from MAFw's own ``[devtools]``
80 optional-dependency group in ``pyproject.toml``, keeping it as the single
81 source of truth. The check is performed only once; subsequent calls return
82 immediately.
84 :raises ImportError: If ``packaging`` or any other package from the
85 ``devtools`` extra cannot be imported.
86 """
87 global _devtools_checked # noqa: PLW0603
88 if _devtools_checked:
89 return
91 # packaging is required to parse the dependency list itself.
92 try:
93 import packaging # noqa: F401
94 except ImportError as exc:
95 raise ImportError(
96 'Unable to import "packaging", which is required by MAFw development tools. '
97 'Install MAFw with the optional [devtools] feature, or invoke the helper via Hatch '
98 '(e.g. "hatch run dev.py3.14:release --help").'
99 ) from exc
101 # Read remaining deps from metadata/pyproject.toml and verify each.
102 import importlib
104 missing: list[str] = []
105 for name in _read_devtools_deps():
106 try:
107 importlib.import_module(name)
108 except ImportError:
109 missing.append(name)
111 if missing:
112 raise ImportError(
113 f'Unable to import optional development dependencies: {", ".join(missing)}. '
114 'Install MAFw with the optional [devtools] feature, or invoke the helper via Hatch '
115 '(e.g. "hatch run dev.py3.14:release --help").'
116 )
118 _devtools_checked = True