Skip to content

Commit db7196f

Browse files
authored
Add uv_resolution as testenv parameter (#39)
* Add uv_resolution as testenv parameter uv supports defining a custom resolution strategy, defaulting to 'highest', and allowing 'lowest' or 'lowest-direct' at the time of writing, which provides an alternative to package constraints enabling lowest direct or transitive dependencies to be installed. This change provides a new 'uv_resolution' configuration option for test environments, which if defined will apply the '--resolution' option to uv to enable this behavior. Implements #28. * Ensure call arguments match defined options for uv resolution * Restrict uv resolution options to known values uv currently supports "highest", "lowest", or "lowest-direct" as resolution strategies, which we now validate. * Document the new `uv_resolution` flag * Use base type and post-process for uv_resolution mypy expects the actual type for add_config, so we instead use post-processing to validate the literal string against known options, raising an error if otherwise. * Test all valid and an invalid uv_resolution strategy
1 parent b67f324 commit db7196f

File tree

3 files changed

+77
-3
lines changed

3 files changed

+77
-3
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,17 @@ python -m tox r -e py312 # will use uv
2828
This flag, set on a tox environment level, controls if the created virtual environment injects pip/setuptools/wheel into
2929
the created virtual environment or not. By default, is off. You will need to set this if you have a project that uses
3030
the old legacy editable mode, or your project does not support the `pyproject.toml` powered isolated build model.
31+
32+
### uv_resolution
33+
34+
This flag, set on a tox environment level, informs uv of the desired [resolution strategy]:
35+
36+
- `highest` - (default) selects the highest version of a package that satisfies the constraints
37+
- `lowest` - install the **lowest** compatible versions for all dependencies, both **direct** and **transitive**
38+
- `lowest-direct` - opt for the **lowest** compatible versions for all **direct** dependencies, while using the
39+
**latest** compatible versions for all **transitive** dependencies
40+
41+
This is a uv specific feature that may be used as an alternative to frozen constraints for test environments, if the
42+
intention is to validate the lower bounds of your dependencies during test executions.
43+
44+
[resolution strategy]: https://github.com/astral-sh/uv/blob/0.1.20/README.md#resolution-strategy

src/tox_uv/_installer.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tox.config.of_type import ConfigDynamicDefinition
1212
from tox.config.types import Command
1313
from tox.execute.request import StdinSource
14-
from tox.tox_env.errors import Recreate
14+
from tox.tox_env.errors import Fail, Recreate
1515
from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage
1616
from tox.tox_env.python.pip.pip_install import Pip
1717
from tox.tox_env.python.pip.req_file import PythonDeps
@@ -27,6 +27,21 @@ class UvInstaller(Pip):
2727

2828
def _register_config(self) -> None:
2929
super()._register_config()
30+
31+
def uv_resolution_post_process(value: str) -> str:
32+
valid_opts = {"highest", "lowest", "lowest-direct"}
33+
if value and value not in valid_opts:
34+
msg = f"Invalid value for uv_resolution: {value!r}. Valid options are: {', '.join(valid_opts)}."
35+
raise Fail(msg)
36+
return value
37+
38+
self._env.conf.add_config(
39+
keys=["uv_resolution"],
40+
of_type=str,
41+
default="",
42+
desc="Define the resolution strategy for uv",
43+
post_process=uv_resolution_post_process,
44+
)
3045
if self._with_list_deps: # pragma: no branch
3146
conf = cast(ConfigDynamicDefinition[Command], self._env.conf._defined["list_dependencies_command"]) # noqa: SLF001
3247
conf.default = Command([self.uv, "--color", "never", "pip", "freeze"])
@@ -44,16 +59,22 @@ def default_install_command(self, conf: Config, env_name: str | None) -> Command
4459
def post_process_install_command(self, cmd: Command) -> Command:
4560
install_command = cmd.args
4661
pip_pre: bool = self._env.conf["pip_pre"]
62+
uv_resolution: str = self._env.conf["uv_resolution"]
4763
try:
4864
opts_at = install_command.index("{opts}")
4965
except ValueError:
5066
if pip_pre:
5167
install_command.extend(("--prerelease", "allow"))
68+
if uv_resolution:
69+
install_command.extend(("--resolution", uv_resolution))
5270
else:
5371
if pip_pre:
5472
install_command[opts_at] = "--prerelease"
5573
install_command.insert(opts_at + 1, "allow")
56-
else:
74+
if uv_resolution:
75+
install_command[opts_at] = "--resolution"
76+
install_command.insert(opts_at + 1, uv_resolution)
77+
if not (pip_pre or uv_resolution):
5778
install_command.pop(opts_at)
5879
return cmd
5980

tests/test_tox_uv_installer.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import sys
44
from typing import TYPE_CHECKING
55

6+
import pytest
7+
68
if TYPE_CHECKING:
7-
import pytest
89
from tox.pytest import ToxProjectCreator
910

1011

@@ -60,3 +61,41 @@ def test_uv_install_without_pre_custom_install_cmd(tox_project: ToxProjectCreato
6061
})
6162
result = project.run("-vv")
6263
result.assert_success()
64+
65+
66+
@pytest.mark.parametrize("strategy", ["highest", "lowest", "lowest-direct"])
67+
def test_uv_install_with_resolution_strategy(tox_project: ToxProjectCreator, strategy: str) -> None:
68+
project = tox_project({"tox.ini": f"[testenv]\ndeps = tomli>=2.0.1\npackage = skip\nuv_resolution = {strategy}"})
69+
execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None)
70+
71+
result = project.run("-vv")
72+
result.assert_success()
73+
74+
assert execute_calls.call_args[0][3].cmd[2:] == ["install", "--resolution", strategy, "tomli>=2.0.1", "-v"]
75+
76+
77+
def test_uv_install_with_invalid_resolution_strategy(tox_project: ToxProjectCreator) -> None:
78+
project = tox_project({"tox.ini": "[testenv]\ndeps = tomli>=2.0.1\npackage = skip\nuv_resolution = invalid"})
79+
80+
result = project.run("-vv")
81+
result.assert_failed(code=1)
82+
83+
assert "Invalid value for uv_resolution: 'invalid'." in result.out
84+
85+
86+
def test_uv_install_with_resolution_strategy_custom_install_cmd(tox_project: ToxProjectCreator) -> None:
87+
project = tox_project({
88+
"tox.ini": """
89+
[testenv]
90+
deps = tomli>=2.0.1
91+
package = skip
92+
uv_resolution = lowest-direct
93+
install_command = uv pip install {packages}
94+
"""
95+
})
96+
execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None)
97+
98+
result = project.run("-vv")
99+
result.assert_success()
100+
101+
assert execute_calls.call_args[0][3].cmd[2:] == ["install", "tomli>=2.0.1", "--resolution", "lowest-direct"]

0 commit comments

Comments
 (0)