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)