Skip to content

Commit

Permalink
Add uv lock support
Browse files Browse the repository at this point in the history
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Sep 19, 2024
1 parent 77ff40c commit 587c7f8
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 44 deletions.
10 changes: 4 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
74 changes: 64 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
[![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.

<!--ts-->

- [How to use](#how-to-use)
- [Configuration](#configuration)
- [uv_seed](#uv_seed)
- [uv_resolution](#uv_resolution)
- [uv_python_preference](#uv_python_preference)
<!--te-->

## How to use

Expand All @@ -19,10 +28,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
Expand All @@ -45,11 +102,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.
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
39 changes: 21 additions & 18 deletions src/tox_uv/_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,40 @@

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.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: ANN401
raise NotImplementedError # not supported


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:
Expand All @@ -42,13 +57,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}"]
Expand Down Expand Up @@ -78,12 +86,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)
Expand Down Expand Up @@ -138,5 +140,6 @@ def _install_list_of_deps( # noqa: C901


__all__ = [
"ReadOnlyUvInstaller",
"UvInstaller",
]
61 changes: 61 additions & 0 deletions src/tox_uv/_run_lock.py
Original file line number Diff line number Diff line change
@@ -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:
raise NotImplementedError

@property
def _external_pkg_tox_env_type(self) -> str:
raise NotImplementedError

def _build_packages(self) -> list[Package]:
raise NotImplementedError

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], sorted(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",
]
8 changes: 5 additions & 3 deletions src/tox_uv/_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from uv import find_uv_bin
from virtualenv.discovery.py_spec import PythonSpec

from ._installer import UvInstaller
from ._installer import ReadOnlyUvInstaller, UvInstaller

if sys.version_info >= (3, 10): # pragma: no cover (py310+)
from typing import TypeAlias
Expand All @@ -45,9 +45,11 @@


class UvVenv(Python, ABC):
InstallerClass: type[ReadOnlyUvInstaller] = UvInstaller

def __init__(self, create_args: ToxEnvCreateArgs) -> None:
self._executor: Execute | None = None
self._installer: UvInstaller | None = None
self._installer: ReadOnlyUvInstaller | None = None
self._created = False
super().__init__(create_args)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/tox_uv/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 587c7f8

Please sign in to comment.