Skip to content

Commit

Permalink
Install package support for lock files
Browse files Browse the repository at this point in the history
Signed-off-by: Bernát Gábor <[email protected]>
  • Loading branch information
gaborbernat committed Sep 19, 2024
1 parent 619796b commit f811383
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 44 deletions.
85 changes: 59 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,37 @@
[![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.lock support](#uvlock-support)
- [tox environment types provided](#tox-environment-types-provided)
- [uv.lock support](#uvlock-support)
- [extras](#extras)
- [with_dev](#with_dev)
- [External package support](#external-package-support)
- [Environment creation](#environment-creation)
- [uv_seed](#uv_seed)
- [uv_resolution](#uv_resolution)
- [uv_python_preference](#uv_python_preference)
- [Package installation](#package-installation)
- [uv_resolution](#uv_resolution)
<!--te-->

## How to use

Install `tox-uv` into the environment of your tox and it will replace virtualenv and pip for all runs:
Install `tox-uv` into the environment of your tox, and it will replace `virtualenv` and `pip` for all runs:

```bash
python -m pip install tox-uv
python -m tox r -e py312 # will use uv
uv tool install tox --with tox-uv # use uv to install
tox --version # validate you are using the installed tox
tox r -e py312 # will use uv
```

## Configuration
## tox environment types provided

This package will provide the following new tox environments:

- `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.
Expand All @@ -37,7 +45,7 @@ python -m tox r -e py312 # will use uv
- `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
## 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
Expand Down Expand Up @@ -79,32 +87,57 @@ In this example:
`test` and `type` extra groups.

Note that when using `uv-venv-lock-runner`, _all_ dependencies will come from the lock file, controlled by `extras`.
Therefore, options like `deps` are ignored.
Therefore, options like `deps` are ignored (and all others
[enumerated here](https://tox.wiki/en/stable/config.html#python-run) as Python run flags).

### `extras`

Select which extra groups you want to install with `uv sync`. By default, is empty.

### `with_dev`

A boolean flag to toggle installation of the `uv` development dependencies. By default, is false.

### External package support

Should tox be invoked with the [`--installpkg`](https://tox.wiki/en/stable/cli_interface.html#tox-run---installpkg) flag
(the argument **must** be either a wheel or source distribution) the sync operation will run with `--no-install-project`
and `uv pip install` will be used afterward to install the provided package.

## Environment creation

We use `uv venv` to create virtual environments. This process can be configured with the following options:

### uv_seed
### `uv_seed`

This flag, set on a tox environment level, controls if the created virtual environment injects pip/setuptools/wheel into
the created virtual environment or not. By default, is off. You will need to set this if you have a project that uses
the old legacy editable mode, or your project does not support the `pyproject.toml` powered isolated build model.
This flag, set on a tox environment level, controls if the created virtual environment injects `pip`, `setuptools` and
`wheel` into the created virtual environment or not. By default, is off. You will need to set this if you have a project
that uses the old legacy editable mode, or your project does not support the `pyproject.toml` powered isolated build
model.

### uv_resolution
### `uv_python_preference`

This flag, set on a tox environment level, informs uv of the desired [resolution strategy]:
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.

## Package installation

We use `uv pip` to install packages into the virtual environment. The behavior of this can be configured via the
following options:

### `uv_resolution`

This flag, set on a tox environment level, informs `uv` of the desired [resolution strategy]:

- `highest` - (default) selects the highest version of a package that satisfies the constraints
- `lowest` - install the **lowest** compatible versions for all dependencies, both **direct** and **transitive**
- `lowest-direct` - opt for the **lowest** compatible versions for all **direct** dependencies, while using the
**latest** compatible versions for all **transitive** dependencies

This is a uv specific feature that may be used as an alternative to frozen constraints for test environments, if the
This is an `uv` specific feature that may be used as an alternative to frozen constraints for test environments, if the
intention is to validate the lower bounds of your dependencies during test executions.

[resolution strategy]: https://github.com/astral-sh/uv/blob/0.1.20/README.md#resolution-strategy

### uv_python_preference

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.
14 changes: 4 additions & 10 deletions src/tox_uv/_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from tox.config.types import Command
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, PythonInstallerListDependencies
from tox.tox_env.python.pip.pip_install import Pip
from tox.tox_env.python.pip.req_file import PythonDeps
from uv import find_uv_bin

Expand All @@ -21,7 +21,9 @@
from tox.tox_env.python.api import Python


class ReadOnlyUvInstaller(PythonInstallerListDependencies):
class UvInstaller(Pip):
"""Pip is a python installer that can install packages as defined by PEP-508 and PEP-517."""

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)
Expand All @@ -33,13 +35,6 @@ def freeze_cmd(self) -> list[str]:
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:
super()._register_config()

Expand Down Expand Up @@ -140,6 +135,5 @@ def _install_list_of_deps( # noqa: C901


__all__ = [
"ReadOnlyUvInstaller",
"UvInstaller",
]
12 changes: 9 additions & 3 deletions src/tox_uv/_run_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Set, cast

from tox.execute.request import StdinSource
from tox.tox_env.python.package import SdistPackage, WheelPackage
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"
Expand Down Expand Up @@ -54,8 +53,15 @@ def _setup_env(self) -> None:
cmd.extend(("--extra", extra))
if not self.conf["with_dev"]:
cmd.append("--no-dev")
install_pkg = getattr(self.options, "install_pkg", None)
if install_pkg is not None:
cmd.append("--no-install-project")
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="uv-sync", show=False)
outcome.assert_success()
if install_pkg is not None:
path = Path(install_pkg)
pkg = (WheelPackage if path.suffix == ".whl" else SdistPackage)(path, deps=[])
self.installer.install([pkg], "install-pkg", of_type="external")

@property
def environment_variables(self) -> dict[str, str]:
Expand Down
8 changes: 3 additions & 5 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 ReadOnlyUvInstaller, UvInstaller
from ._installer import UvInstaller

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


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

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

Expand Down Expand Up @@ -91,7 +89,7 @@ def executor(self) -> Execute:
@property
def installer(self) -> Installer[Any]:
if self._installer is None:
self._installer = self.InstallerClass(self)
self._installer = UvInstaller(self)
return self._installer

@property
Expand Down
39 changes: 39 additions & 0 deletions tests/test_tox_uv_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
from typing import TYPE_CHECKING

import pytest
from uv import find_uv_bin

if TYPE_CHECKING:
Expand Down Expand Up @@ -91,3 +92,41 @@ def test_uv_lock_with_dev(tox_project: ToxProjectCreator) -> None:
("py", "uv-sync", ["uv", "sync", "--frozen"]),
]
assert calls == expected


@pytest.mark.parametrize(
"name",
[
"tox_uv-1.12.2-py3-none-any.whl",
"tox_uv-1.12.2.tar.gz",
],
)
def test_uv_lock_with_install_pkg(tox_project: ToxProjectCreator, name: str) -> None:
project = tox_project({
"tox.ini": """
[testenv]
runner = uv-venv-lock-runner
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
wheel = project.path / name
wheel.write_text("")
result = project.run("-vv", "run", "--installpkg", str(wheel))
result.assert_success()

calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[uv, "venv", "-p", sys.executable, "--allow-existing", "-v", str(project.path / ".tox" / "py")],
),
("py", "uv-sync", ["uv", "sync", "--frozen", "--no-dev", "--no-install-project"]),
(
"py",
"install_external",
[uv, "pip", "install", "--reinstall", "--no-deps", f"tox-uv@{wheel}", "-v"],
),
]
assert calls == expected

0 comments on commit f811383

Please sign in to comment.