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
« 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**.
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.
12Update logic (inherited from :class:`~mafw.devtools.toolchain.ProjectTool`):
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.
18Post-update validation (overridden here):
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.
25The revert pattern uses ``_revert()``
26which restores the raw pyproject.toml content saved before the update.
27"""
29from __future__ import annotations
31import subprocess
33from mafw.devtools import DevtoolsError
34from mafw.devtools.toolchain import ProjectTool
35from mafw.devtools.toolchain.base import Issue
38class PreCommitTool(ProjectTool):
39 """Manage pre-commit's version lower bound in pyproject.toml.
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.
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.
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 """
59 @property
60 def package_name(self) -> str:
61 """PyPI package name.
63 :return: ``"pre-commit"``
64 :rtype: str
65 """
66 return 'pre-commit'
68 @property
69 def section_path(self) -> str:
70 """TOML section path where pre-commit is declared.
72 :return: ``"project.optional-dependencies.dev"``
73 :rtype: str
74 """
75 return 'project.optional-dependencies.dev'
77 @property
78 def env_name(self) -> str:
79 """Hatch environment where pre-commit hooks are run.
81 :return: ``"dev"``
82 :rtype: str
83 """
84 return 'dev'
86 def post_update(self) -> None:
87 """Recreate the dev environment, then run pre-commit hooks; revert on failure.
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.
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()
103 # Validate dependency resolution via temporary clone.
104 self._recreate_environment(self.env_name, py_version)
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 )
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)
126 def verify(self) -> list[Issue]:
127 """Return an empty list — pre-commit is referenced in a single location.
129 pre-commit has no cross-file consistency to verify (unlike ruff which
130 appears in both pyproject.toml and ``.pre-commit-config.yaml``).
132 :return: An empty list of issues.
133 :rtype: list[toolchain.Issue]
134 """
135 return []