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

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**. 

6 

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. 

11 

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

13 

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. 

17 

18Post-update validation (overridden here): 

19 

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. 

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 

35 

36 

37class SphinxTool(ProjectTool): 

38 """Manage Sphinx's version lower bound in pyproject.toml. 

39 

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. 

46 

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. 

50 

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 """ 

55 

56 @property 

57 def package_name(self) -> str: 

58 """PyPI package name for Sphinx. 

59 

60 :return: ``"sphinx"`` 

61 :rtype: str 

62 """ 

63 return 'sphinx' 

64 

65 @property 

66 def section_path(self) -> str: 

67 """Dot-separated TOML path to the dependency array containing Sphinx. 

68 

69 :return: ``"project.optional-dependencies.doc"`` 

70 :rtype: str 

71 """ 

72 return 'project.optional-dependencies.doc' 

73 

74 @property 

75 def env_name(self) -> str: 

76 """Hatch environment where sphinx documentation is built. 

77 

78 :return: ``"dev"`` 

79 :rtype: str 

80 """ 

81 return 'dev' 

82 

83 def post_update(self) -> None: 

84 """Recreate the doc environment and run the documentation build. 

85 

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. 

89 

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. 

95 

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() 

102 

103 # Validate dependency resolution via temporary clone. 

104 self._recreate_environment(self.env_name, py_version) 

105 

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 ) 

115 

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)