Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Install package support for lock files #96

Merged
merged 1 commit into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 65 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,51 @@
[![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.
environments not using a 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
environments using `uv.lock` (note we can’t 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
## 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:
`runner` to `uv-venv-lock-runner`. Furthermore, should in such environments you use the `extras` config to instruct `uv`
to install the specified extras, for example:

```ini

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).

### uv_seed
### `extras`

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.
A list of string that selects, which extra groups you want to install with `uv sync`. By default, it is empty.

### uv_resolution
### `with_dev`

This flag, set on a tox environment level, informs uv of the desired [resolution strategy]:
A boolean flag to toggle installation of the `uv` development dependencies. By default, it is false.

- `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
### External package support

This is a 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.
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.

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

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

### `uv_seed`

### uv_python_preference
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, it is off. You will need to set this if you have a
project that uses the old legacy-editable mode, or your project doesn’t support the `pyproject.toml` powered isolated
build model.

This flag, set on a tox environment level, controls how uv select the Python interpreter.
### `uv_python_preference`

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
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 is 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 satisfying 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 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
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