diff --git a/README.md b/README.md index 1a0e2f0..bdd2218 100644 --- a/README.md +++ b/README.md @@ -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`. - [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) ## 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 @@ -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 diff --git a/src/tox_uv/_installer.py b/src/tox_uv/_installer.py index 44a6cbd..976380e 100644 --- a/src/tox_uv/_installer.py +++ b/src/tox_uv/_installer.py @@ -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 @@ -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) @@ -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() @@ -140,6 +135,5 @@ def _install_list_of_deps( # noqa: C901 __all__ = [ - "ReadOnlyUvInstaller", "UvInstaller", ] diff --git a/src/tox_uv/_run_lock.py b/src/tox_uv/_run_lock.py index 6854a3b..c043ec1 100644 --- a/src/tox_uv/_run_lock.py +++ b/src/tox_uv/_run_lock.py @@ -2,13 +2,14 @@ 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: @@ -16,8 +17,6 @@ class UvVenvLockRunner(UvVenv, RunToxEnv): - InstallerClass = ReadOnlyUvInstaller - @staticmethod def id() -> str: return "uv-venv-lock-runner" @@ -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]: diff --git a/src/tox_uv/_venv.py b/src/tox_uv/_venv.py index f168666..f69e8c5 100644 --- a/src/tox_uv/_venv.py +++ b/src/tox_uv/_venv.py @@ -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 @@ -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) @@ -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 diff --git a/tests/test_tox_uv_lock.py b/tests/test_tox_uv_lock.py index f41d0c0..212703a 100644 --- a/tests/test_tox_uv_lock.py +++ b/tests/test_tox_uv_lock.py @@ -3,6 +3,7 @@ import sys from typing import TYPE_CHECKING +import pytest from uv import find_uv_bin if TYPE_CHECKING: @@ -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