Coverage for src/mafw/devtools/toolchain/tools/precommit.py: 100%

29 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-26 09:13 +0000

1# Copyright 2026 European Union 

2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu) 

3# SPDX-License-Identifier: EUPL-1.2 

4""" 

5Concrete :class:`~mafw.devtools.toolchain.ProjectTool` implementation for **pre-commit**. 

6 

7pre-commit is a project tool whose version lower bound is declared in 

8``project.optional-dependencies.dev``. This module manages the ``>=`` 

9specifier for pre-commit and validates updates by running 

10``pre-commit run --all-files`` within the recreated ``dev`` hatch environment. 

11 

12Update logic (inherited from :class:`~mafw.devtools.toolchain.ProjectTool`): 

13 

141. Query PyPI for the latest stable pre-commit release. 

152. Compare against the current lower bound in pyproject.toml. 

163. If a newer version exists, update the ``>=`` specifier. 

17 

18Post-update validation (overridden here): 

19 

204. Force recreation of the ``dev`` hatch environment to ensure fresh 

21 dependency resolution with the new pre-commit version. 

225. Run ``pre-commit run --all-files`` via the recreated dev environment. 

236. If the command fails, revert pyproject.toml to its previous content. 

24 

25The revert pattern uses ``_revert()`` 

26which restores the raw pyproject.toml content saved before the update. 

27""" 

28 

29from __future__ import annotations 

30 

31import subprocess 

32 

33from mafw.devtools import DevtoolsError 

34from mafw.devtools.toolchain import ProjectTool 

35from mafw.devtools.toolchain.base import Issue 

36 

37 

38class PreCommitTool(ProjectTool): 

39 """Manage pre-commit's version lower bound in pyproject.toml. 

40 

41 pre-commit is categorized as a *project* tool because its version is 

42 controlled through the ``dev`` optional-dependencies group in 

43 ``pyproject.toml``. After an update, the ``dev`` hatch environment is 

44 recreated to ensure fresh dependency resolution, and then 

45 ``pre-commit run --all-files`` is executed to validate compatibility. 

46 If the command fails, pyproject.toml is reverted to its pre-update state. 

47 

48 All standard behaviour (version detection, update, bootstrap) is provided 

49 by :class:`~mafw.devtools.toolchain.ProjectTool`. 

50 The :meth:`post_update` is overridden to run pre-commit hooks, and 

51 :meth:`verify` explicitly returns an empty list since pre-commit is 

52 referenced in a single location. 

53 

54 :param project_root: Path to the project root directory containing 

55 ``pyproject.toml``. Defaults to the current working directory. 

56 :type project_root: Path | None 

57 """ 

58 

59 @property 

60 def package_name(self) -> str: 

61 """PyPI package name. 

62 

63 :return: ``"pre-commit"`` 

64 :rtype: str 

65 """ 

66 return 'pre-commit' 

67 

68 @property 

69 def section_path(self) -> str: 

70 """TOML section path where pre-commit is declared. 

71 

72 :return: ``"project.optional-dependencies.dev"`` 

73 :rtype: str 

74 """ 

75 return 'project.optional-dependencies.dev' 

76 

77 @property 

78 def env_name(self) -> str: 

79 """Hatch environment where pre-commit hooks are run. 

80 

81 :return: ``"dev"`` 

82 :rtype: str 

83 """ 

84 return 'dev' 

85 

86 def post_update(self) -> None: 

87 """Recreate the dev environment, then run pre-commit hooks; revert on failure. 

88 

89 Forces the recreation of the ``dev`` hatch environment to ensure 

90 the newly specified pre-commit version is resolved and installed. 

91 Then runs ``pre-commit run --all-files`` via the ``dev`` environment 

92 to validate compatibility. If the command fails (non-zero exit code), 

93 pyproject.toml is reverted to its saved pre-update content and a 

94 :class:`~mafw.devtools.DevtoolsError` is raised. 

95 

96 :raises DevtoolsError: If environment recreation fails or if 

97 ``pre-commit run --all-files`` exits with a non-zero code. 

98 """ 

99 # Force fresh dependency resolution with the new version. 

100 # Determine the highest supported Python version dynamically. 

101 py_version = self._get_highest_python_version() 

102 

103 # Validate dependency resolution via temporary clone. 

104 self._recreate_environment(self.env_name, py_version) 

105 

106 # Run pre-commit hooks via the recreated dev environment. 

107 result = subprocess.run( 

108 ['hatch', 'run', f'dev.py{py_version}:pre-commit', 'run', '--all-files'], # noqa: S603, S607 

109 capture_output=True, 

110 text=True, 

111 check=False, 

112 cwd=self._project_root, 

113 ) 

114 

115 if result.returncode != 0: 

116 # Parse resolution failure for detailed error reporting. 

117 conflict = self._parse_resolution_failure(result.stderr) 

118 # Revert pyproject.toml to its pre-update state. 

119 self._revert() 

120 msg = f'pre-commit run --all-files failed after updating {self.name} — pyproject.toml has been reverted.' 

121 if conflict: 

122 msg += f'\nConflicting package: {conflict}' 

123 msg += f'\n{result.stderr.strip()}' 

124 raise DevtoolsError(msg) 

125 

126 def verify(self) -> list[Issue]: 

127 """Return an empty list — pre-commit is referenced in a single location. 

128 

129 pre-commit has no cross-file consistency to verify (unlike ruff which 

130 appears in both pyproject.toml and ``.pre-commit-config.yaml``). 

131 

132 :return: An empty list of issues. 

133 :rtype: list[toolchain.Issue] 

134 """ 

135 return []