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)