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

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. 

6 

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. 

11 

12Use :func:`ensure_devtools_available` as a guard before importing subpackages. 

13""" 

14 

15from __future__ import annotations 

16 

17 

18class DevtoolsError(Exception): 

19 """Base exception for MAFw development tool errors. 

20 

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 """ 

25 

26 

27_devtools_checked: bool = False 

28"""Module-level flag to skip repeated import checks after the first successful call.""" 

29 

30 

31def _read_devtools_deps() -> list[str]: 

32 """Read the ``devtools`` optional-dependency names from MAFw package metadata. 

33 

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. 

37 

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). 

41 

42 :return: List of top-level package names required by the ``devtools`` extra. 

43 :rtype: list[str] 

44 """ 

45 import importlib.metadata 

46 

47 from packaging.requirements import Requirement 

48 

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 

61 

62 # Fallback: read pyproject.toml from the source tree. 

63 import tomllib 

64 from pathlib import Path 

65 

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] 

72 

73 return [] 

74 

75 

76def ensure_devtools_available() -> None: 

77 """Verify that optional development dependencies are importable. 

78 

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. 

83 

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 

90 

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 

100 

101 # Read remaining deps from metadata/pyproject.toml and verify each. 

102 import importlib 

103 

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) 

110 

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 ) 

117 

118 _devtools_checked = True