Coverage for src/mafw/devtools/toolchain/tools/uv.py: 100%

48 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.HostTool` implementation for **uv**. 

6 

7uv is a host tool installed system-wide via ``pipx``. Its version must stay 

8in sync with the ``hatch-uv`` dependency declared in pyproject.toml under 

9``[tool.hatch.envs.hatch-uv]``. 

10 

11Update logic: 

12 

131. Run ``pipx upgrade uv`` (via :meth:`~mafw.devtools.toolchain.HostTool.update`) to bring the pipx 

14 installation to the latest version. 

152. Update the lower-bound specifier for uv in the hatch-uv environment 

16 dependencies section of ``pyproject.toml``. 

17 

18Verification logic: 

19 

20Compare the pipx-installed version against the specifier declared in 

21``pyproject.toml`` and report an :class:`~mafw.devtools.toolchain.Issue` if 

22the installed version does not satisfy the declared constraint. 

23""" 

24 

25from __future__ import annotations 

26 

27from pathlib import Path 

28 

29from packaging.specifiers import SpecifierSet 

30from packaging.version import Version 

31 

32from mafw.devtools import DevtoolsError 

33from mafw.devtools.toolchain import HostTool, Issue 

34from mafw.devtools.toolchain.pyproject_modifier import PyprojectModifier 

35 

36_HATCH_UV_SECTION: str = 'tool.hatch.envs.hatch-uv.dependencies' 

37"""Section path in pyproject.toml where the uv dependency is declared.""" 

38 

39_PYPROJECT_PATH: Path = Path('pyproject.toml') 

40"""Default path to the project's pyproject.toml file.""" 

41 

42 

43class UvTool(HostTool): 

44 """Toolchain management for the **uv** package manager. 

45 

46 uv is installed as a host tool via pipx and its version specifier is 

47 additionally tracked in pyproject.toml under the ``hatch-uv`` hatch 

48 environment so that hatch uses a compatible uv version. 

49 

50 All common host-tool operations (bootstrap, version detection, PyPI 

51 queries) are inherited from :class:`~mafw.devtools.toolchain.HostTool`. This subclass overrides 

52 :meth:`update` to additionally synchronize the pyproject.toml specifier 

53 and :meth:`verify` to check specifier satisfaction. 

54 

55 :param pyproject_path: Path to the ``pyproject.toml`` file. 

56 Defaults to ``pyproject.toml`` in the current working directory. 

57 :type pyproject_path: Path 

58 """ 

59 

60 def __init__(self, pyproject_path: Path = _PYPROJECT_PATH) -> None: 

61 self._pyproject_path = pyproject_path 

62 

63 @property 

64 def pipx_package_name(self) -> str: 

65 """The pipx package name for uv.""" 

66 return 'uv' 

67 

68 def update(self) -> bool: 

69 """Upgrade uv via pipx and update the pyproject.toml specifier. 

70 

71 Performs two operations: 

72 

73 1. Delegates to :meth:`~mafw.devtools.toolchain.HostTool.update` which runs ``pipx upgrade uv`` 

74 and checks if the version changed. 

75 2. If the version changed, updates the lower-bound version specifier 

76 for uv in the ``[tool.hatch.envs.hatch-uv]`` dependencies section 

77 of pyproject.toml. 

78 

79 :return: ``True`` if the version changed, ``False`` if already up to date. 

80 :raises DevtoolsError: If pipx is not available, the upgrade fails, 

81 or the pyproject.toml modification fails. 

82 """ 

83 # Step 1: Upgrade via pipx using base class logic. 

84 pipx_changed = super().update() 

85 

86 if not pipx_changed: 

87 return False 

88 

89 # Step 2: Update pyproject.toml lower-bound for hatch-uv environment. 

90 new_version = self.detect_current_version() 

91 if new_version is None: 

92 raise DevtoolsError('uv was upgraded but version detection failed afterwards.') 

93 

94 try: 

95 modifier = PyprojectModifier(self._pyproject_path) 

96 modifier.read() 

97 modifier.update_lower_bound('uv', str(new_version), _HATCH_UV_SECTION) 

98 modifier.write() 

99 except DevtoolsError: 

100 # Requirement 10.7: report partial failure, leave pipx at upgraded version. 

101 raise 

102 

103 return True 

104 

105 def verify(self) -> list[Issue]: 

106 """Check that the pipx-installed uv satisfies the declared specifier. 

107 

108 Compares the version installed via pipx against the version specifier 

109 declared in pyproject.toml under ``[tool.hatch.envs.hatch-uv]``. 

110 

111 :return: A list containing one :class:`~mafw.devtools.toolchain.Issue` 

112 if the versions are out of sync, or an empty list if consistent. 

113 """ 

114 # Get the installed version from pipx. 

115 installed_version: Version | None = self.detect_current_version() 

116 if installed_version is None: 

117 return [ 

118 Issue( 

119 tool_name=self.name, 

120 description='uv is not installed via pipx.', 

121 ) 

122 ] 

123 

124 # Get the declared specifier from pyproject.toml. 

125 modifier = PyprojectModifier(self._pyproject_path) 

126 modifier.read() 

127 raw_specifier = modifier.get_dependency_specifier('uv', _HATCH_UV_SECTION) 

128 

129 # Extract the version specifier portion (everything after the package name). 

130 # The raw specifier looks like "uv>=0.10" or "uv>0.10,<1.0". 

131 spec_str = _extract_specifier_from_requirement(raw_specifier) 

132 

133 # Check if the installed version satisfies the declared specifier. 

134 specifier_set = SpecifierSet(spec_str) 

135 if installed_version not in specifier_set: 

136 return [ 

137 Issue( 

138 tool_name=self.name, 

139 description=( 

140 f'Installed uv version {installed_version} does not satisfy ' 

141 f'the declared specifier "{spec_str}" in ' 

142 f'[tool.hatch.envs.hatch-uv] dependencies.' 

143 ), 

144 ) 

145 ] 

146 

147 return [] 

148 

149 

150def _extract_specifier_from_requirement(requirement: str) -> str: 

151 """Extract the version specifier portion from a PEP 508 requirement string. 

152 

153 Given a string like ``"uv>=0.10"`` or ``"uv>0.10,<1.0"``, returns the 

154 specifier portion (e.g. ``">=0.10"`` or ``">0.10,<1.0"``). 

155 

156 :param requirement: A raw PEP 508 requirement string. 

157 :type requirement: str 

158 :return: The specifier portion of the requirement. 

159 :rtype: str 

160 """ 

161 from packaging.requirements import Requirement 

162 

163 req = Requirement(requirement) 

164 return str(req.specifier)