Source code for mafw.devtools.toolchain.tools.uv
# Copyright 2026 European Union
# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
# SPDX-License-Identifier: EUPL-1.2
"""
Concrete :class:`~mafw.devtools.toolchain.HostTool` implementation for **uv**.
uv is a host tool installed system-wide via ``pipx``. Its version must stay
in sync with the ``hatch-uv`` dependency declared in pyproject.toml under
``[tool.hatch.envs.hatch-uv]``.
Update logic:
1. Run ``pipx upgrade uv`` (via :meth:`~mafw.devtools.toolchain.HostTool.update`) to bring the pipx
installation to the latest version.
2. Update the lower-bound specifier for uv in the hatch-uv environment
dependencies section of ``pyproject.toml``.
Verification logic:
Compare the pipx-installed version against the specifier declared in
``pyproject.toml`` and report an :class:`~mafw.devtools.toolchain.Issue` if
the installed version does not satisfy the declared constraint.
"""
from __future__ import annotations
from pathlib import Path
from packaging.specifiers import SpecifierSet
from packaging.version import Version
from mafw.devtools import DevtoolsError
from mafw.devtools.toolchain import HostTool, Issue
from mafw.devtools.toolchain.pyproject_modifier import PyprojectModifier
_HATCH_UV_SECTION: str = 'tool.hatch.envs.hatch-uv.dependencies'
"""Section path in pyproject.toml where the uv dependency is declared."""
_PYPROJECT_PATH: Path = Path('pyproject.toml')
"""Default path to the project's pyproject.toml file."""
[docs]
class UvTool(HostTool):
"""Toolchain management for the **uv** package manager.
uv is installed as a host tool via pipx and its version specifier is
additionally tracked in pyproject.toml under the ``hatch-uv`` hatch
environment so that hatch uses a compatible uv version.
All common host-tool operations (bootstrap, version detection, PyPI
queries) are inherited from :class:`~mafw.devtools.toolchain.HostTool`. This subclass overrides
:meth:`update` to additionally synchronize the pyproject.toml specifier
and :meth:`verify` to check specifier satisfaction.
:param pyproject_path: Path to the ``pyproject.toml`` file.
Defaults to ``pyproject.toml`` in the current working directory.
:type pyproject_path: Path
"""
def __init__(self, pyproject_path: Path = _PYPROJECT_PATH) -> None:
self._pyproject_path = pyproject_path
@property
def pipx_package_name(self) -> str:
"""The pipx package name for uv."""
return 'uv'
[docs]
def update(self) -> bool:
"""Upgrade uv via pipx and update the pyproject.toml specifier.
Performs two operations:
1. Delegates to :meth:`~mafw.devtools.toolchain.HostTool.update` which runs ``pipx upgrade uv``
and checks if the version changed.
2. If the version changed, updates the lower-bound version specifier
for uv in the ``[tool.hatch.envs.hatch-uv]`` dependencies section
of pyproject.toml.
:return: ``True`` if the version changed, ``False`` if already up to date.
:raises DevtoolsError: If pipx is not available, the upgrade fails,
or the pyproject.toml modification fails.
"""
# Step 1: Upgrade via pipx using base class logic.
pipx_changed = super().update()
if not pipx_changed:
return False
# Step 2: Update pyproject.toml lower-bound for hatch-uv environment.
new_version = self.detect_current_version()
if new_version is None:
raise DevtoolsError('uv was upgraded but version detection failed afterwards.')
try:
modifier = PyprojectModifier(self._pyproject_path)
modifier.read()
modifier.update_lower_bound('uv', str(new_version), _HATCH_UV_SECTION)
modifier.write()
except DevtoolsError:
# Requirement 10.7: report partial failure, leave pipx at upgraded version.
raise
return True
[docs]
def verify(self) -> list[Issue]:
"""Check that the pipx-installed uv satisfies the declared specifier.
Compares the version installed via pipx against the version specifier
declared in pyproject.toml under ``[tool.hatch.envs.hatch-uv]``.
:return: A list containing one :class:`~mafw.devtools.toolchain.Issue`
if the versions are out of sync, or an empty list if consistent.
"""
# Get the installed version from pipx.
installed_version: Version | None = self.detect_current_version()
if installed_version is None:
return [
Issue(
tool_name=self.name,
description='uv is not installed via pipx.',
)
]
# Get the declared specifier from pyproject.toml.
modifier = PyprojectModifier(self._pyproject_path)
modifier.read()
raw_specifier = modifier.get_dependency_specifier('uv', _HATCH_UV_SECTION)
# Extract the version specifier portion (everything after the package name).
# The raw specifier looks like "uv>=0.10" or "uv>0.10,<1.0".
spec_str = _extract_specifier_from_requirement(raw_specifier)
# Check if the installed version satisfies the declared specifier.
specifier_set = SpecifierSet(spec_str)
if installed_version not in specifier_set:
return [
Issue(
tool_name=self.name,
description=(
f'Installed uv version {installed_version} does not satisfy '
f'the declared specifier "{spec_str}" in '
f'[tool.hatch.envs.hatch-uv] dependencies.'
),
)
]
return []
[docs]
def _extract_specifier_from_requirement(requirement: str) -> str:
"""Extract the version specifier portion from a PEP 508 requirement string.
Given a string like ``"uv>=0.10"`` or ``"uv>0.10,<1.0"``, returns the
specifier portion (e.g. ``">=0.10"`` or ``">0.10,<1.0"``).
:param requirement: A raw PEP 508 requirement string.
:type requirement: str
:return: The specifier portion of the requirement.
:rtype: str
"""
from packaging.requirements import Requirement
req = Requirement(requirement)
return str(req.specifier)