From b7bed43518d26956c0949a0a1f0a48111ecf38f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 18 Sep 2024 18:37:08 -0700 Subject: [PATCH] Add uv lock support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .pre-commit-config.yaml | 10 +++--- README.md | 65 +++++++++++++++++++++++++++++++++------ pyproject.toml | 10 +++--- src/tox_uv/_installer.py | 41 +++++++++++++----------- src/tox_uv/_run_lock.py | 61 ++++++++++++++++++++++++++++++++++++ src/tox_uv/_venv.py | 4 ++- src/tox_uv/plugin.py | 2 ++ tests/test_tox_uv_lock.py | 16 ++++++++++ tox.ini | 4 +-- 9 files changed, 171 insertions(+), 42 deletions(-) create mode 100644 src/tox_uv/_run_lock.py create mode 100644 tests/test_tox_uv_lock.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43c6308..8185123 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,12 +15,12 @@ repos: - id: codespell additional_dependencies: ["tomli>=2.0.1"] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: "1.4.0" + rev: "1.4.1" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "2.2.3" + rev: "2.2.4" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit @@ -30,12 +30,10 @@ repos: - id: ruff args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: https://github.com/rbubley/mirrors-prettier - rev: "v3.3.3" # Use the sha / tag you want to point at + rev: "v3.3.3" hooks: - id: prettier - additional_dependencies: - - prettier@3.3.3 - - "@prettier/plugin-xml@3.4.1" + args: ["--print-width=120", "--prose-wrap=always"] - repo: meta hooks: - id: check-hooks-apply diff --git a/README.md b/README.md index 976bf72..d86d8e1 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ [![check](https://github.com/tox-dev/tox-uv/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/tox-uv/actions/workflows/check.yaml) [![Downloads](https://static.pepy.tech/badge/tox-uv/month)](https://pepy.tech/project/tox-uv) -**tox-uv** is a tox plugin which replaces virtualenv and pip with uv in your tox environments. -Note that you will get both the benefits (performance) or downsides (bugs) of uv. +**tox-uv** is a tox plugin which replaces virtualenv and pip with uv in your tox environments. Note that you will get +both the benefits (performance) or downsides (bugs) of uv. ## How to use @@ -19,10 +19,58 @@ python -m tox r -e py312 # will use uv ## Configuration -- `uv-venv-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner). +- `uv-venv-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner) for + environments not using lock file. +- `uv-venv-lock-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner) for + environments using `uv.lock` (note we cannot detect the presence of the `uv.lock` file to enable this because that + would break environments not using the lock file - such as your linter). - `uv-venv-pep-517` is the ID for the PEP-517 packaging environment. - `uv-venv-cmd-builder` is the ID for the external cmd builder. +### `uv.lock` support + +If you want for a tox environment to use `uv sync` with a `uv.lock` file you need to change for that tox environment the +`runner` to `uv-venv-lock-runner`. Furthermore, should in such environments you can use the `extras` config to instruct +`uv` to also install the specified extras, for example: + +```ini + +[testenv:fix] +description = run code formatter and linter (auto-fix) +skip_install = true +deps = + pre-commit-uv>=4.1.1 +set_env = + {[testenv]set_env} + NPM_CONFIG_REGISTRY = {env:NPM_CONFIG_REGISTRY:http://artprod.dev.bloomberg.com/artifactory/api/npm/npm-repos} +commands = + pre-commit run --all-files --show-diff-on-failure + +[testenv:type] +runner = uv-venv-lock-runner +description = run type checker via mypy +commands = + mypy {posargs:src} + +[testenv:dev] +runner = uv-venv-lock-runner +description = dev environment +extras = + dev + test + type +commands = + uv pip tree +``` + +In this example: + +- `fix` will use the `uv-venv-runner` and use `uv pip install` to install dependencies to the environment. +- `type` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment without any + extra group. +- `dev` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment with the `dev`, + `test` and `type` extra groups. + ### uv_seed This flag, set on a tox environment level, controls if the created virtual environment injects pip/setuptools/wheel into @@ -45,11 +93,8 @@ intention is to validate the lower bounds of your dependencies during test execu ### uv_python_preference -This flag, set on a tox environment level, controls how uv select the Python -interpreter. +This flag, set on a tox environment level, controls how uv select the Python interpreter. -By default, uv will attempt to use Python versions found on the system and only -download managed interpreters when necessary. However, It's possible to adjust -uv's Python version selection preference with the -[python-preference](https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences) -option. +By default, uv will attempt to use Python versions found on the system and only download managed interpreters when +necessary. However, It's possible to adjust uv's Python version selection preference with the +[python-preference](https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences) option. diff --git a/pyproject.toml b/pyproject.toml index c7be88e..f0e6ca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,17 +41,17 @@ dynamic = [ "version", ] dependencies = [ - "importlib-resources>=6.4.4; python_version<'3.9'", + "importlib-resources>=6.4.5; python_version<'3.9'", "packaging>=24.1", - "tox<5,>=4.18", + "tox<5,>=4.20", "typing-extensions>=4.12.2; python_version<'3.10'", - "uv<1,>=0.4.7", + "uv<1,>=0.4.12", ] optional-dependencies.testing = [ "covdefaults>=2.3", - "devpi-process>=1", + "devpi-process>=1.0.2", "diff-cover>=9.2", - "pytest>=8.3.2", + "pytest>=8.3.3", "pytest-cov>=5", "pytest-mock>=3.14", ] diff --git a/src/tox_uv/_installer.py b/src/tox_uv/_installer.py index 913bd41..24bcdcc 100644 --- a/src/tox_uv/_installer.py +++ b/src/tox_uv/_installer.py @@ -4,25 +4,42 @@ import logging from collections import defaultdict -from typing import TYPE_CHECKING, Any, Sequence, cast +from typing import TYPE_CHECKING, Any, Sequence from packaging.requirements import Requirement from packaging.utils import parse_sdist_filename, parse_wheel_filename -from tox.config.of_type import ConfigDynamicDefinition from tox.config.types import Command -from tox.execute.request import StdinSource +from tox.report import HandledError from tox.tox_env.errors import Fail, Recreate from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage -from tox.tox_env.python.pip.pip_install import Pip +from tox.tox_env.python.pip.pip_install import Pip, PythonInstallerListDependencies from tox.tox_env.python.pip.req_file import PythonDeps from uv import find_uv_bin if TYPE_CHECKING: from tox.config.main import Config from tox.tox_env.package import PathPackage + from tox.tox_env.python.api import Python -class UvInstaller(Pip): +class ReadOnlyUvInstaller(PythonInstallerListDependencies): + def __init__(self, tox_env: Python, with_list_deps: bool = True) -> None: # noqa: FBT001, FBT002 + self._with_list_deps = with_list_deps + super().__init__(tox_env) + + def freeze_cmd(self) -> list[str]: + return [self.uv, "--color", "never", "pip", "freeze"] + + @property + def uv(self) -> str: + return find_uv_bin() + + def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: PLR6301, ARG002, ANN401 + msg = "Install operation not supported" + raise HandledError(msg) + + +class UvInstaller(ReadOnlyUvInstaller, Pip): """Pip is a python installer that can install packages as defined by PEP-508 and PEP-517.""" def _register_config(self) -> None: @@ -42,13 +59,6 @@ def uv_resolution_post_process(value: str) -> str: desc="Define the resolution strategy for uv", post_process=uv_resolution_post_process, ) - if self._with_list_deps: # pragma: no branch - conf = cast(ConfigDynamicDefinition[Command], self._env.conf._defined["list_dependencies_command"]) # noqa: SLF001 - conf.default = Command([self.uv, "--color", "never", "pip", "freeze"]) - - @property - def uv(self) -> str: - return find_uv_bin() def default_install_command(self, conf: Config, env_name: str | None) -> Command: # noqa: ARG002 cmd = [self.uv, "pip", "install", "{opts}", "{packages}"] @@ -78,12 +88,6 @@ def post_process_install_command(self, cmd: Command) -> Command: install_command.pop(opts_at) return cmd - def installed(self) -> list[str]: - cmd: Command = self._env.conf["list_dependencies_command"] - result = self._env.execute(cmd=cmd.args, stdin=StdinSource.OFF, run_id="freeze", show=False) - result.assert_success() - return result.out.splitlines() - def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: ANN401 if isinstance(arguments, PythonDeps): self._install_requirement_file(arguments, section, of_type) @@ -138,5 +142,6 @@ def _install_list_of_deps( # noqa: C901 __all__ = [ + "ReadOnlyUvInstaller", "UvInstaller", ] diff --git a/src/tox_uv/_run_lock.py b/src/tox_uv/_run_lock.py new file mode 100644 index 0000000..ebd2c09 --- /dev/null +++ b/src/tox_uv/_run_lock.py @@ -0,0 +1,61 @@ +"""GitHub Actions integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Set, cast + +from tox.execute.request import StdinSource +from tox.tox_env.python.runner import add_extras_to_env, add_skip_missing_interpreters_to_core +from tox.tox_env.runner import RunToxEnv + +from ._installer import ReadOnlyUvInstaller +from ._venv import UvVenv + +if TYPE_CHECKING: + from tox.tox_env.package import Package + + +class UvVenvLockRunner(UvVenv, RunToxEnv): + InstallerClass = ReadOnlyUvInstaller + + @staticmethod + def id() -> str: + return "uv-venv-lock-runner" + + def _register_package_conf(self) -> bool: # noqa: PLR6301 + return False + + @property + def _package_tox_env_type(self) -> str: + return "" # no packaging per _register_package_conf above + + @property + def _external_pkg_tox_env_type(self) -> str: + return "" # no packaging per _register_package_conf above # pragma: no cover + + def _build_packages(self) -> list[Package]: # noqa: PLR6301 + return [] + + def register_config(self) -> None: + super().register_config() + add_extras_to_env(self.conf) + add_skip_missing_interpreters_to_core(self.core, self.options) + + def _setup_env(self) -> None: + super()._setup_env() + cmd = ["uv", "sync", "--frozen"] + for extra in cast(Set[str], self.conf["extras"]): + cmd.extend(("--extra", extra)) + outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="uv-sync", show=False) + outcome.assert_success() + + @property + def environment_variables(self) -> dict[str, str]: + env = super().environment_variables + env["UV_PROJECT_ENVIRONMENT"] = str(self.venv_dir) + return env + + +__all__ = [ + "UvVenvLockRunner", +] diff --git a/src/tox_uv/_venv.py b/src/tox_uv/_venv.py index f69e8c5..cee480c 100644 --- a/src/tox_uv/_venv.py +++ b/src/tox_uv/_venv.py @@ -45,6 +45,8 @@ class UvVenv(Python, ABC): + InstallerClass = UvInstaller + def __init__(self, create_args: ToxEnvCreateArgs) -> None: self._executor: Execute | None = None self._installer: UvInstaller | None = None @@ -89,7 +91,7 @@ def executor(self) -> Execute: @property def installer(self) -> Installer[Any]: if self._installer is None: - self._installer = UvInstaller(self) + self._installer = self.InstallerClass(self) return self._installer @property diff --git a/src/tox_uv/plugin.py b/src/tox_uv/plugin.py index e7ac27e..c7574ec 100644 --- a/src/tox_uv/plugin.py +++ b/src/tox_uv/plugin.py @@ -9,6 +9,7 @@ from ._package import UvVenvCmdBuilder, UvVenvPep517Packager from ._run import UvVenvRunner +from ._run_lock import UvVenvLockRunner if TYPE_CHECKING: from tox.tox_env.register import ToxEnvRegister @@ -17,6 +18,7 @@ @impl def tox_register_tox_env(register: ToxEnvRegister) -> None: register.add_run_env(UvVenvRunner) + register.add_run_env(UvVenvLockRunner) register.add_package_env(UvVenvPep517Packager) register.add_package_env(UvVenvCmdBuilder) register._default_run_env = UvVenvRunner.id() # noqa: SLF001 diff --git a/tests/test_tox_uv_lock.py b/tests/test_tox_uv_lock.py new file mode 100644 index 0000000..956d0b2 --- /dev/null +++ b/tests/test_tox_uv_lock.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from tox.execute import ExecuteRequest + from tox.pytest import ToxProjectCreator + + +def test_uv_lock_list_dependencies_command(tox_project: ToxProjectCreator) -> None: + project = tox_project({"tox.ini": "[testenv]\nrunner=uv-venv-lock"}) + execute_calls = project.patch_execute(lambda r: 0 if r.run_id in {"uv-sync", "freeze"} else None) + result = project.run("--list-dependencies", "-vv") + result.assert_success() + request: ExecuteRequest = execute_calls.call_args[0][3] + assert request.cmd[1:] == ["--color", "never", "pip", "freeze"] diff --git a/tox.ini b/tox.ini index ccba69f..5fe9f50 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ commands = description = format the code base to adhere to our styles, and complain about what we cannot do automatically skip_install = true deps = - pre-commit-uv>=4.1.1 + pre-commit-uv>=4.1.2 commands = pre-commit run --all-files --show-diff-on-failure @@ -56,7 +56,7 @@ skip_install = true deps = check-wheel-contents>=0.6 twine>=5.1.1 - uv>=0.4.10 + uv>=0.4.12 commands = uv build --sdist --wheel --out-dir {env_tmp_dir} . twine check {env_tmp_dir}{/}*