Source code for mafw.devtools.toolchain.tools.sphinx

#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""
Concrete :class:`~mafw.devtools.toolchain.ProjectTool` implementation for **Sphinx**.

Sphinx is a project tool whose version lower bound is declared in
``project.optional-dependencies.doc``. This module manages the ``>=``
specifier for Sphinx and validates updates by running the documentation
build via hatch.

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

1. Query PyPI for the latest stable Sphinx release.
2. Compare against the current lower bound in pyproject.toml.
3. If a newer version exists, update the ``>=`` specifier.

Post-update validation (overridden here):

4. Determine the highest supported Python version dynamically.
5. Recreate the ``dev.py<version>`` hatch environment to force fresh dependency resolution.
6. Run ``hatch run dev.py<version>:doc`` to build the documentation.
7. If the build fails, revert pyproject.toml to its previous content.

The revert pattern uses ``_revert()``
which restores the raw pyproject.toml content saved before the update.
"""

from __future__ import annotations

import subprocess

from mafw.devtools import DevtoolsError
from mafw.devtools.toolchain import ProjectTool


[docs] class SphinxTool(ProjectTool): """Manage Sphinx's version lower bound in pyproject.toml. Sphinx is categorized as a *project* tool because its version is controlled through the ``doc`` optional-dependencies group in ``pyproject.toml``. After an update, the ``doc`` hatch environment is recreated and the documentation build is executed to validate compatibility. If the build fails, pyproject.toml is reverted to its pre-update state. All standard behaviour (version detection, update, verify, bootstrap) is provided by :class:`~mafw.devtools.toolchain.ProjectTool`. Only :meth:`post_update` is overridden to run the documentation build. :param project_root: Path to the project root directory containing ``pyproject.toml``. Defaults to the current working directory. :type project_root: Path | None """ @property def package_name(self) -> str: """PyPI package name for Sphinx. :return: ``"sphinx"`` :rtype: str """ return 'sphinx' @property def section_path(self) -> str: """Dot-separated TOML path to the dependency array containing Sphinx. :return: ``"project.optional-dependencies.doc"`` :rtype: str """ return 'project.optional-dependencies.doc' @property def env_name(self) -> str: """Hatch environment where sphinx documentation is built. :return: ``"dev"`` :rtype: str """ return 'dev'
[docs] def post_update(self) -> None: """Recreate the doc environment and run the documentation build. Forces recreation of the ``doc`` hatch environment to ensure the newly specified dependency versions are resolved and installed. Then executes ``hatch run dev.py3.14:doc`` from the project root. If the documentation build fails (non-zero exit code), the pyproject.toml is reverted to its saved pre-update content and a :class:`~mafw.devtools.DevtoolsError` is raised. The error message includes the conflicting package name when the failure is caused by a dependency resolution conflict. :raises DevtoolsError: If environment recreation or the documentation build fails after the update. """ # Force fresh dependency resolution with the new Sphinx version. # Determine the highest supported Python version dynamically. py_version = self._get_highest_python_version() # Validate dependency resolution via temporary clone. self._recreate_environment(self.env_name, py_version) # Run the documentation build using the determined Python version. doc_build_cmd = ['hatch', 'run', f'dev.py{py_version}:doc'] result = subprocess.run( doc_build_cmd, # noqa: S603 capture_output=True, text=True, check=False, cwd=self._project_root, ) if result.returncode != 0: # Attempt to identify the conflicting package from stderr. conflict = self._parse_resolution_failure(result.stderr) # Revert pyproject.toml to its pre-update state. self._revert() msg = f'Documentation build failed after updating {self.name} — pyproject.toml has been reverted.' if conflict: msg += f'\nConflicting package: {conflict}' msg += f'\n{result.stderr.strip()}' raise DevtoolsError(msg)