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
« 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**.
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]``.
11Update logic:
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``.
18Verification logic:
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"""
25from __future__ import annotations
27from pathlib import Path
29from packaging.specifiers import SpecifierSet
30from packaging.version import Version
32from mafw.devtools import DevtoolsError
33from mafw.devtools.toolchain import HostTool, Issue
34from mafw.devtools.toolchain.pyproject_modifier import PyprojectModifier
36_HATCH_UV_SECTION: str = 'tool.hatch.envs.hatch-uv.dependencies'
37"""Section path in pyproject.toml where the uv dependency is declared."""
39_PYPROJECT_PATH: Path = Path('pyproject.toml')
40"""Default path to the project's pyproject.toml file."""
43class UvTool(HostTool):
44 """Toolchain management for the **uv** package manager.
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.
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.
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 """
60 def __init__(self, pyproject_path: Path = _PYPROJECT_PATH) -> None:
61 self._pyproject_path = pyproject_path
63 @property
64 def pipx_package_name(self) -> str:
65 """The pipx package name for uv."""
66 return 'uv'
68 def update(self) -> bool:
69 """Upgrade uv via pipx and update the pyproject.toml specifier.
71 Performs two operations:
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.
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()
86 if not pipx_changed:
87 return False
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.')
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
103 return True
105 def verify(self) -> list[Issue]:
106 """Check that the pipx-installed uv satisfies the declared specifier.
108 Compares the version installed via pipx against the version specifier
109 declared in pyproject.toml under ``[tool.hatch.envs.hatch-uv]``.
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 ]
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)
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)
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 ]
147 return []
150def _extract_specifier_from_requirement(requirement: str) -> str:
151 """Extract the version specifier portion from a PEP 508 requirement string.
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"``).
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
163 req = Requirement(requirement)
164 return str(req.specifier)