Coverage for src/mafw/devtools/toolchain/tools/sphinx.py: 100%
27 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 **Sphinx**.
7Sphinx is a project tool whose version lower bound is declared in
8``project.optional-dependencies.doc``. This module manages the ``>=``
9specifier for Sphinx and validates updates by running the documentation
10build via hatch.
12Update logic (inherited from :class:`~mafw.devtools.toolchain.ProjectTool`):
141. Query PyPI for the latest stable Sphinx 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. Determine the highest supported Python version dynamically.
215. Recreate the ``dev.py<version>`` hatch environment to force fresh dependency resolution.
226. Run ``hatch run dev.py<version>:doc`` to build the documentation.
237. If the build 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
37class SphinxTool(ProjectTool):
38 """Manage Sphinx's version lower bound in pyproject.toml.
40 Sphinx is categorized as a *project* tool because its version is
41 controlled through the ``doc`` optional-dependencies group in
42 ``pyproject.toml``. After an update, the ``doc`` hatch environment is
43 recreated and the documentation build is executed to validate
44 compatibility. If the build fails, pyproject.toml is reverted to its
45 pre-update state.
47 All standard behaviour (version detection, update, verify, bootstrap)
48 is provided by :class:`~mafw.devtools.toolchain.ProjectTool`.
49 Only :meth:`post_update` is overridden to run the documentation build.
51 :param project_root: Path to the project root directory containing
52 ``pyproject.toml``. Defaults to the current working directory.
53 :type project_root: Path | None
54 """
56 @property
57 def package_name(self) -> str:
58 """PyPI package name for Sphinx.
60 :return: ``"sphinx"``
61 :rtype: str
62 """
63 return 'sphinx'
65 @property
66 def section_path(self) -> str:
67 """Dot-separated TOML path to the dependency array containing Sphinx.
69 :return: ``"project.optional-dependencies.doc"``
70 :rtype: str
71 """
72 return 'project.optional-dependencies.doc'
74 @property
75 def env_name(self) -> str:
76 """Hatch environment where sphinx documentation is built.
78 :return: ``"dev"``
79 :rtype: str
80 """
81 return 'dev'
83 def post_update(self) -> None:
84 """Recreate the doc environment and run the documentation build.
86 Forces recreation of the ``doc`` hatch environment to ensure
87 the newly specified dependency versions are resolved and installed.
88 Then executes ``hatch run dev.py3.14:doc`` from the project root.
90 If the documentation build fails (non-zero exit code), the
91 pyproject.toml is reverted to its saved pre-update content and a
92 :class:`~mafw.devtools.DevtoolsError` is raised. The error message
93 includes the conflicting package name when the failure is caused
94 by a dependency resolution conflict.
96 :raises DevtoolsError: If environment recreation or the documentation
97 build fails after the update.
98 """
99 # Force fresh dependency resolution with the new Sphinx 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 the documentation build using the determined Python version.
107 doc_build_cmd = ['hatch', 'run', f'dev.py{py_version}:doc']
108 result = subprocess.run(
109 doc_build_cmd, # noqa: S603
110 capture_output=True,
111 text=True,
112 check=False,
113 cwd=self._project_root,
114 )
116 if result.returncode != 0:
117 # Attempt to identify the conflicting package from stderr.
118 conflict = self._parse_resolution_failure(result.stderr)
119 # Revert pyproject.toml to its pre-update state.
120 self._revert()
121 msg = f'Documentation build failed after updating {self.name} — pyproject.toml has been reverted.'
122 if conflict:
123 msg += f'\nConflicting package: {conflict}'
124 msg += f'\n{result.stderr.strip()}'
125 raise DevtoolsError(msg)