Skip to content

Commit 397bba7

Browse files
authored
Install package support for lock files (#96)
1 parent 619796b commit 397bba7

File tree

5 files changed

+120
-50
lines changed

5 files changed

+120
-50
lines changed

README.md

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,51 @@
55
[![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)
66
[![Downloads](https://static.pepy.tech/badge/tox-uv/month)](https://pepy.tech/project/tox-uv)
77

8-
**tox-uv** is a tox plugin which replaces virtualenv and pip with uv in your tox environments. Note that you will get
9-
both the benefits (performance) or downsides (bugs) of uv.
8+
**tox-uv** is a `tox` plugin, which replaces `virtualenv` and pip with `uv` in your `tox` environments. Note that you
9+
will get both the benefits (performance) or downsides (bugs) of `uv`.
1010

1111
<!--ts-->
1212

1313
- [How to use](#how-to-use)
14-
- [Configuration](#configuration)
15-
- [uv.lock support](#uvlock-support)
14+
- [tox environment types provided](#tox-environment-types-provided)
15+
- [uv.lock support](#uvlock-support)
16+
- [extras](#extras)
17+
- [with_dev](#with_dev)
18+
- [External package support](#external-package-support)
19+
- [Environment creation](#environment-creation)
1620
- [uv_seed](#uv_seed)
17-
- [uv_resolution](#uv_resolution)
1821
- [uv_python_preference](#uv_python_preference)
22+
- [Package installation](#package-installation)
23+
- [uv_resolution](#uv_resolution)
1924
<!--te-->
2025

2126
## How to use
2227

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

2530
```bash
26-
python -m pip install tox-uv
27-
python -m tox r -e py312 # will use uv
31+
uv tool install tox --with tox-uv # use uv to install
32+
tox --version # validate you are using the installed tox
33+
tox r -e py312 # will use uv
2834
```
2935

30-
## Configuration
36+
## tox environment types provided
37+
38+
This package will provide the following new tox environments:
3139

3240
- `uv-venv-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner) for
33-
environments not using lock file.
41+
environments not using a lock file.
3442
- `uv-venv-lock-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner) for
35-
environments using `uv.lock` (note we cannot detect the presence of the `uv.lock` file to enable this because that
43+
environments using `uv.lock` (note we can’t detect the presence of the `uv.lock` file to enable this because that
3644
would break environments not using the lock file - such as your linter).
3745
- `uv-venv-pep-517` is the ID for the PEP-517 packaging environment.
3846
- `uv-venv-cmd-builder` is the ID for the external cmd builder.
3947

40-
### uv.lock support
48+
## uv.lock support
4149

4250
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
43-
`runner` to `uv-venv-lock-runner`. Furthermore, should in such environments you can use the `extras` config to instruct
44-
`uv` to also install the specified extras, for example:
51+
`runner` to `uv-venv-lock-runner`. Furthermore, should in such environments you use the `extras` config to instruct `uv`
52+
to install the specified extras, for example:
4553

4654
```ini
4755

@@ -79,32 +87,57 @@ In this example:
7987
`test` and `type` extra groups.
8088

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

84-
### uv_seed
93+
### `extras`
8594

86-
This flag, set on a tox environment level, controls if the created virtual environment injects pip/setuptools/wheel into
87-
the created virtual environment or not. By default, is off. You will need to set this if you have a project that uses
88-
the old legacy editable mode, or your project does not support the `pyproject.toml` powered isolated build model.
95+
A list of string that selects, which extra groups you want to install with `uv sync`. By default, it is empty.
8996

90-
### uv_resolution
97+
### `with_dev`
9198

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

94-
- `highest` - (default) selects the highest version of a package that satisfies the constraints
95-
- `lowest` - install the **lowest** compatible versions for all dependencies, both **direct** and **transitive**
96-
- `lowest-direct` - opt for the **lowest** compatible versions for all **direct** dependencies, while using the
97-
**latest** compatible versions for all **transitive** dependencies
101+
### External package support
98102

99-
This is a uv specific feature that may be used as an alternative to frozen constraints for test environments, if the
100-
intention is to validate the lower bounds of your dependencies during test executions.
103+
Should tox be invoked with the [`--installpkg`](https://tox.wiki/en/stable/cli_interface.html#tox-run---installpkg) flag
104+
(the argument **must** be either a wheel or source distribution) the sync operation will run with `--no-install-project`
105+
and `uv pip install` will be used afterward to install the provided package.
101106

102-
[resolution strategy]: https://github.com/astral-sh/uv/blob/0.1.20/README.md#resolution-strategy
107+
## Environment creation
108+
109+
We use `uv venv` to create virtual environments. This process can be configured with the following options:
110+
111+
### `uv_seed`
103112

104-
### uv_python_preference
113+
This flag, set on a tox environment level, controls if the created virtual environment injects `pip`, `setuptools` and
114+
`wheel` into the created virtual environment or not. By default, it is off. You will need to set this if you have a
115+
project that uses the old legacy-editable mode, or your project doesn’t support the `pyproject.toml` powered isolated
116+
build model.
105117

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

108-
By default, uv will attempt to use Python versions found on the system and only download managed interpreters when
109-
necessary. However, It's possible to adjust uv's Python version selection preference with the
120+
This flag, set on a tox environment level, controls how `uv` select the Python interpreter.
121+
122+
By default, `uv` will attempt to use Python versions found on the system and only download managed interpreters when
123+
necessary. However, It is possible to adjust `uv`'s Python version selection preference with the
110124
[python-preference](https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences) option.
125+
126+
## Package installation
127+
128+
We use `uv pip` to install packages into the virtual environment. The behavior of this can be configured via the
129+
following options:
130+
131+
### `uv_resolution`
132+
133+
This flag, set on a tox environment level, informs `uv` of the desired [resolution strategy]:
134+
135+
- `highest` - (default) selects the highest version of a package satisfying the constraints.
136+
- `lowest` - install the **lowest** compatible versions for all dependencies, both **direct** and **transitive**.
137+
- `lowest-direct` - opt for the **lowest** compatible versions for all **direct** dependencies, while using the
138+
**latest** compatible versions for all **transitive** dependencies.
139+
140+
This is an `uv` specific feature that may be used as an alternative to frozen constraints for test environments if the
141+
intention is to validate the lower bounds of your dependencies during test executions.
142+
143+
[resolution strategy]: https://github.com/astral-sh/uv/blob/0.1.20/README.md#resolution-strategy

src/tox_uv/_installer.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tox.config.types import Command
1212
from tox.tox_env.errors import Fail, Recreate
1313
from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage
14-
from tox.tox_env.python.pip.pip_install import Pip, PythonInstallerListDependencies
14+
from tox.tox_env.python.pip.pip_install import Pip
1515
from tox.tox_env.python.pip.req_file import PythonDeps
1616
from uv import find_uv_bin
1717

@@ -21,7 +21,9 @@
2121
from tox.tox_env.python.api import Python
2222

2323

24-
class ReadOnlyUvInstaller(PythonInstallerListDependencies):
24+
class UvInstaller(Pip):
25+
"""Pip is a python installer that can install packages as defined by PEP-508 and PEP-517."""
26+
2527
def __init__(self, tox_env: Python, with_list_deps: bool = True) -> None: # noqa: FBT001, FBT002
2628
self._with_list_deps = with_list_deps
2729
super().__init__(tox_env)
@@ -33,13 +35,6 @@ def freeze_cmd(self) -> list[str]:
3335
def uv(self) -> str:
3436
return find_uv_bin()
3537

36-
def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: ANN401
37-
raise NotImplementedError # not supported
38-
39-
40-
class UvInstaller(ReadOnlyUvInstaller, Pip):
41-
"""Pip is a python installer that can install packages as defined by PEP-508 and PEP-517."""
42-
4338
def _register_config(self) -> None:
4439
super()._register_config()
4540

@@ -140,6 +135,5 @@ def _install_list_of_deps( # noqa: C901
140135

141136

142137
__all__ = [
143-
"ReadOnlyUvInstaller",
144138
"UvInstaller",
145139
]

src/tox_uv/_run_lock.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@
22

33
from __future__ import annotations
44

5+
from pathlib import Path
56
from typing import TYPE_CHECKING, Set, cast
67

78
from tox.execute.request import StdinSource
9+
from tox.tox_env.python.package import SdistPackage, WheelPackage
810
from tox.tox_env.python.runner import add_extras_to_env, add_skip_missing_interpreters_to_core
911
from tox.tox_env.runner import RunToxEnv
1012

11-
from ._installer import ReadOnlyUvInstaller
1213
from ._venv import UvVenv
1314

1415
if TYPE_CHECKING:
1516
from tox.tox_env.package import Package
1617

1718

1819
class UvVenvLockRunner(UvVenv, RunToxEnv):
19-
InstallerClass = ReadOnlyUvInstaller
20-
2120
@staticmethod
2221
def id() -> str:
2322
return "uv-venv-lock-runner"
@@ -54,8 +53,15 @@ def _setup_env(self) -> None:
5453
cmd.extend(("--extra", extra))
5554
if not self.conf["with_dev"]:
5655
cmd.append("--no-dev")
56+
install_pkg = getattr(self.options, "install_pkg", None)
57+
if install_pkg is not None:
58+
cmd.append("--no-install-project")
5759
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="uv-sync", show=False)
5860
outcome.assert_success()
61+
if install_pkg is not None:
62+
path = Path(install_pkg)
63+
pkg = (WheelPackage if path.suffix == ".whl" else SdistPackage)(path, deps=[])
64+
self.installer.install([pkg], "install-pkg", of_type="external")
5965

6066
@property
6167
def environment_variables(self) -> dict[str, str]:

src/tox_uv/_venv.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from uv import find_uv_bin
1919
from virtualenv.discovery.py_spec import PythonSpec
2020

21-
from ._installer import ReadOnlyUvInstaller, UvInstaller
21+
from ._installer import UvInstaller
2222

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

4646

4747
class UvVenv(Python, ABC):
48-
InstallerClass: type[ReadOnlyUvInstaller] = UvInstaller
49-
5048
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
5149
self._executor: Execute | None = None
52-
self._installer: ReadOnlyUvInstaller | None = None
50+
self._installer: UvInstaller | None = None
5351
self._created = False
5452
super().__init__(create_args)
5553

@@ -91,7 +89,7 @@ def executor(self) -> Execute:
9189
@property
9290
def installer(self) -> Installer[Any]:
9391
if self._installer is None:
94-
self._installer = self.InstallerClass(self)
92+
self._installer = UvInstaller(self)
9593
return self._installer
9694

9795
@property

tests/test_tox_uv_lock.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
from typing import TYPE_CHECKING
55

6+
import pytest
67
from uv import find_uv_bin
78

89
if TYPE_CHECKING:
@@ -91,3 +92,41 @@ def test_uv_lock_with_dev(tox_project: ToxProjectCreator) -> None:
9192
("py", "uv-sync", ["uv", "sync", "--frozen"]),
9293
]
9394
assert calls == expected
95+
96+
97+
@pytest.mark.parametrize(
98+
"name",
99+
[
100+
"tox_uv-1.12.2-py3-none-any.whl",
101+
"tox_uv-1.12.2.tar.gz",
102+
],
103+
)
104+
def test_uv_lock_with_install_pkg(tox_project: ToxProjectCreator, name: str) -> None:
105+
project = tox_project({
106+
"tox.ini": """
107+
[testenv]
108+
runner = uv-venv-lock-runner
109+
"""
110+
})
111+
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
112+
wheel = project.path / name
113+
wheel.write_text("")
114+
result = project.run("-vv", "run", "--installpkg", str(wheel))
115+
result.assert_success()
116+
117+
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
118+
uv = find_uv_bin()
119+
expected = [
120+
(
121+
"py",
122+
"venv",
123+
[uv, "venv", "-p", sys.executable, "--allow-existing", "-v", str(project.path / ".tox" / "py")],
124+
),
125+
("py", "uv-sync", ["uv", "sync", "--frozen", "--no-dev", "--no-install-project"]),
126+
(
127+
"py",
128+
"install_external",
129+
[uv, "pip", "install", "--reinstall", "--no-deps", f"tox-uv@{wheel}", "-v"],
130+
),
131+
]
132+
assert calls == expected

0 commit comments

Comments
 (0)