Skip to content

Commit de98767

Browse files
authored
Add uv lock support (#91)
1 parent 77ff40c commit de98767

File tree

9 files changed

+230
-44
lines changed

9 files changed

+230
-44
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ repos:
1515
- id: codespell
1616
additional_dependencies: ["tomli>=2.0.1"]
1717
- repo: https://github.com/tox-dev/tox-ini-fmt
18-
rev: "1.4.0"
18+
rev: "1.4.1"
1919
hooks:
2020
- id: tox-ini-fmt
2121
args: ["-p", "fix"]
2222
- repo: https://github.com/tox-dev/pyproject-fmt
23-
rev: "2.2.3"
23+
rev: "2.2.4"
2424
hooks:
2525
- id: pyproject-fmt
2626
- repo: https://github.com/astral-sh/ruff-pre-commit
@@ -30,12 +30,10 @@ repos:
3030
- id: ruff
3131
args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
3232
- repo: https://github.com/rbubley/mirrors-prettier
33-
rev: "v3.3.3" # Use the sha / tag you want to point at
33+
rev: "v3.3.3"
3434
hooks:
3535
- id: prettier
36-
additional_dependencies:
37-
38-
- "@prettier/[email protected]"
36+
args: ["--print-width=120", "--prose-wrap=always"]
3937
- repo: meta
4038
hooks:
4139
- id: check-hooks-apply

README.md

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
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.
9-
Note that you will get 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 will get
9+
both the benefits (performance) or downsides (bugs) of uv.
10+
11+
<!--ts-->
12+
13+
- [How to use](#how-to-use)
14+
- [Configuration](#configuration)
15+
- [uv.lock support](#uvlock-support)
16+
- [uv_seed](#uv_seed)
17+
- [uv_resolution](#uv_resolution)
18+
- [uv_python_preference](#uv_python_preference)
19+
<!--te-->
1020

1121
## How to use
1222

@@ -19,10 +29,55 @@ python -m tox r -e py312 # will use uv
1929

2030
## Configuration
2131

22-
- `uv-venv-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner).
32+
- `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.
34+
- `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
36+
would break environments not using the lock file - such as your linter).
2337
- `uv-venv-pep-517` is the ID for the PEP-517 packaging environment.
2438
- `uv-venv-cmd-builder` is the ID for the external cmd builder.
2539

40+
### uv.lock support
41+
42+
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:
45+
46+
```ini
47+
48+
[testenv:fix]
49+
description = run code formatter and linter (auto-fix)
50+
skip_install = true
51+
deps =
52+
pre-commit-uv>=4.1.1
53+
commands =
54+
pre-commit run --all-files --show-diff-on-failure
55+
56+
[testenv:type]
57+
runner = uv-venv-lock-runner
58+
description = run type checker via mypy
59+
commands =
60+
mypy {posargs:src}
61+
62+
[testenv:dev]
63+
runner = uv-venv-lock-runner
64+
description = dev environment
65+
extras =
66+
dev
67+
test
68+
type
69+
commands =
70+
uv pip tree
71+
```
72+
73+
In this example:
74+
75+
- `fix` will use the `uv-venv-runner` and use `uv pip install` to install dependencies to the environment.
76+
- `type` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment without any
77+
extra group.
78+
- `dev` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment with the `dev`,
79+
`test` and `type` extra groups.
80+
2681
### uv_seed
2782

2883
This flag, set on a tox environment level, controls if the created virtual environment injects pip/setuptools/wheel into
@@ -45,11 +100,8 @@ intention is to validate the lower bounds of your dependencies during test execu
45100

46101
### uv_python_preference
47102

48-
This flag, set on a tox environment level, controls how uv select the Python
49-
interpreter.
103+
This flag, set on a tox environment level, controls how uv select the Python interpreter.
50104

51-
By default, uv will attempt to use Python versions found on the system and only
52-
download managed interpreters when necessary. However, It's possible to adjust
53-
uv's Python version selection preference with the
54-
[python-preference](https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences)
55-
option.
105+
By default, uv will attempt to use Python versions found on the system and only download managed interpreters when
106+
necessary. However, It's possible to adjust uv's Python version selection preference with the
107+
[python-preference](https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences) option.

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ dynamic = [
4141
"version",
4242
]
4343
dependencies = [
44-
"importlib-resources>=6.4.4; python_version<'3.9'",
44+
"importlib-resources>=6.4.5; python_version<'3.9'",
4545
"packaging>=24.1",
46-
"tox<5,>=4.18",
46+
"tox<5,>=4.20",
4747
"typing-extensions>=4.12.2; python_version<'3.10'",
48-
"uv<1,>=0.4.7",
48+
"uv<1,>=0.4.12",
4949
]
5050
optional-dependencies.testing = [
5151
"covdefaults>=2.3",
52-
"devpi-process>=1",
52+
"devpi-process>=1.0.2",
5353
"diff-cover>=9.2",
54-
"pytest>=8.3.2",
54+
"pytest>=8.3.3",
5555
"pytest-cov>=5",
5656
"pytest-mock>=3.14",
5757
]

src/tox_uv/_installer.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,40 @@
44

55
import logging
66
from collections import defaultdict
7-
from typing import TYPE_CHECKING, Any, Sequence, cast
7+
from typing import TYPE_CHECKING, Any, Sequence
88

99
from packaging.requirements import Requirement
1010
from packaging.utils import parse_sdist_filename, parse_wheel_filename
11-
from tox.config.of_type import ConfigDynamicDefinition
1211
from tox.config.types import Command
13-
from tox.execute.request import StdinSource
1412
from tox.tox_env.errors import Fail, Recreate
1513
from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage
16-
from tox.tox_env.python.pip.pip_install import Pip
14+
from tox.tox_env.python.pip.pip_install import Pip, PythonInstallerListDependencies
1715
from tox.tox_env.python.pip.req_file import PythonDeps
1816
from uv import find_uv_bin
1917

2018
if TYPE_CHECKING:
2119
from tox.config.main import Config
2220
from tox.tox_env.package import PathPackage
21+
from tox.tox_env.python.api import Python
2322

2423

25-
class UvInstaller(Pip):
24+
class ReadOnlyUvInstaller(PythonInstallerListDependencies):
25+
def __init__(self, tox_env: Python, with_list_deps: bool = True) -> None: # noqa: FBT001, FBT002
26+
self._with_list_deps = with_list_deps
27+
super().__init__(tox_env)
28+
29+
def freeze_cmd(self) -> list[str]:
30+
return [self.uv, "--color", "never", "pip", "freeze"]
31+
32+
@property
33+
def uv(self) -> str:
34+
return find_uv_bin()
35+
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):
2641
"""Pip is a python installer that can install packages as defined by PEP-508 and PEP-517."""
2742

2843
def _register_config(self) -> None:
@@ -42,13 +57,6 @@ def uv_resolution_post_process(value: str) -> str:
4257
desc="Define the resolution strategy for uv",
4358
post_process=uv_resolution_post_process,
4459
)
45-
if self._with_list_deps: # pragma: no branch
46-
conf = cast(ConfigDynamicDefinition[Command], self._env.conf._defined["list_dependencies_command"]) # noqa: SLF001
47-
conf.default = Command([self.uv, "--color", "never", "pip", "freeze"])
48-
49-
@property
50-
def uv(self) -> str:
51-
return find_uv_bin()
5260

5361
def default_install_command(self, conf: Config, env_name: str | None) -> Command: # noqa: ARG002
5462
cmd = [self.uv, "pip", "install", "{opts}", "{packages}"]
@@ -78,12 +86,6 @@ def post_process_install_command(self, cmd: Command) -> Command:
7886
install_command.pop(opts_at)
7987
return cmd
8088

81-
def installed(self) -> list[str]:
82-
cmd: Command = self._env.conf["list_dependencies_command"]
83-
result = self._env.execute(cmd=cmd.args, stdin=StdinSource.OFF, run_id="freeze", show=False)
84-
result.assert_success()
85-
return result.out.splitlines()
86-
8789
def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: ANN401
8890
if isinstance(arguments, PythonDeps):
8991
self._install_requirement_file(arguments, section, of_type)
@@ -138,5 +140,6 @@ def _install_list_of_deps( # noqa: C901
138140

139141

140142
__all__ = [
143+
"ReadOnlyUvInstaller",
141144
"UvInstaller",
142145
]

src/tox_uv/_run_lock.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""GitHub Actions integration."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Set, cast
6+
7+
from tox.execute.request import StdinSource
8+
from tox.tox_env.python.runner import add_extras_to_env, add_skip_missing_interpreters_to_core
9+
from tox.tox_env.runner import RunToxEnv
10+
11+
from ._installer import ReadOnlyUvInstaller
12+
from ._venv import UvVenv
13+
14+
if TYPE_CHECKING:
15+
from tox.tox_env.package import Package
16+
17+
18+
class UvVenvLockRunner(UvVenv, RunToxEnv):
19+
InstallerClass = ReadOnlyUvInstaller
20+
21+
@staticmethod
22+
def id() -> str:
23+
return "uv-venv-lock-runner"
24+
25+
def _register_package_conf(self) -> bool: # noqa: PLR6301
26+
return False
27+
28+
@property
29+
def _package_tox_env_type(self) -> str:
30+
raise NotImplementedError
31+
32+
@property
33+
def _external_pkg_tox_env_type(self) -> str:
34+
raise NotImplementedError
35+
36+
def _build_packages(self) -> list[Package]:
37+
raise NotImplementedError
38+
39+
def register_config(self) -> None:
40+
super().register_config()
41+
add_extras_to_env(self.conf)
42+
add_skip_missing_interpreters_to_core(self.core, self.options)
43+
44+
def _setup_env(self) -> None:
45+
super()._setup_env()
46+
cmd = ["uv", "sync", "--frozen"]
47+
for extra in cast(Set[str], sorted(self.conf["extras"])):
48+
cmd.extend(("--extra", extra))
49+
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="uv-sync", show=False)
50+
outcome.assert_success()
51+
52+
@property
53+
def environment_variables(self) -> dict[str, str]:
54+
env = super().environment_variables
55+
env["UV_PROJECT_ENVIRONMENT"] = str(self.venv_dir)
56+
return env
57+
58+
59+
__all__ = [
60+
"UvVenvLockRunner",
61+
]

src/tox_uv/_venv.py

Lines changed: 5 additions & 3 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 UvInstaller
21+
from ._installer import ReadOnlyUvInstaller, UvInstaller
2222

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

4646

4747
class UvVenv(Python, ABC):
48+
InstallerClass: type[ReadOnlyUvInstaller] = UvInstaller
49+
4850
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
4951
self._executor: Execute | None = None
50-
self._installer: UvInstaller | None = None
52+
self._installer: ReadOnlyUvInstaller | None = None
5153
self._created = False
5254
super().__init__(create_args)
5355

@@ -89,7 +91,7 @@ def executor(self) -> Execute:
8991
@property
9092
def installer(self) -> Installer[Any]:
9193
if self._installer is None:
92-
self._installer = UvInstaller(self)
94+
self._installer = self.InstallerClass(self)
9395
return self._installer
9496

9597
@property

src/tox_uv/plugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from ._package import UvVenvCmdBuilder, UvVenvPep517Packager
1111
from ._run import UvVenvRunner
12+
from ._run_lock import UvVenvLockRunner
1213

1314
if TYPE_CHECKING:
1415
from tox.tox_env.register import ToxEnvRegister
@@ -17,6 +18,7 @@
1718
@impl
1819
def tox_register_tox_env(register: ToxEnvRegister) -> None:
1920
register.add_run_env(UvVenvRunner)
21+
register.add_run_env(UvVenvLockRunner)
2022
register.add_package_env(UvVenvPep517Packager)
2123
register.add_package_env(UvVenvCmdBuilder)
2224
register._default_run_env = UvVenvRunner.id() # noqa: SLF001

0 commit comments

Comments
 (0)