pax_global_header00006660000000000000000000000064147561256470014533gustar00rootroot0000000000000052 comment=4218b8705287628e59b51d5462bf0df61313a7a0 tox-uv-1.25.0/000077500000000000000000000000001475612564700130625ustar00rootroot00000000000000tox-uv-1.25.0/.github/000077500000000000000000000000001475612564700144225ustar00rootroot00000000000000tox-uv-1.25.0/.github/CODEOWNERS000066400000000000000000000000251475612564700160120ustar00rootroot00000000000000* @gaborbernat tox-uv-1.25.0/.github/FUNDING.yml000066400000000000000000000000261475612564700162350ustar00rootroot00000000000000tidelift: pypi/tox-uv tox-uv-1.25.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475612564700166055ustar00rootroot00000000000000tox-uv-1.25.0/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000011411475612564700212120ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "" labels: bug assignees: "" --- ## Issue ## Environment Provide at least: - OS:
Output of pip list of the host Python, where tox is installed ```console ```
## Output of running tox
Output of tox -rvv ```console ```
## Minimal example ```console ``` tox-uv-1.25.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000011521475612564700205740ustar00rootroot00000000000000# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true # default contact_links: - name: 🤷💻🤦 Discussions url: https://github.com/tox-dev/tox-uv/discussions about: | Ask typical Q&A here. Please note that we cannot give support about Python packaging in general, questions about structuring projects and so on. - name: 📝 PyPA Code of Conduct url: https://www.pypa.io/en/latest/code-of-conduct/ about: ❤ Be nice to other members of the community. ☮ Behave. tox-uv-1.25.0/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000013711475612564700222520ustar00rootroot00000000000000--- name: Feature request about: Suggest an enhancement for this project title: "" labels: enhancement assignees: "" --- ## What's the problem this feature will solve? ## Describe the solution you'd like ## Alternative Solutions ## Additional context tox-uv-1.25.0/.github/SECURITY.md000066400000000000000000000005551475612564700162200ustar00rootroot00000000000000# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.0.0 + | :white_check_mark: | | < 1.0.0 | :x: | ## Reporting a Vulnerability To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. tox-uv-1.25.0/.github/dependabot.yml000066400000000000000000000001651475612564700172540ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" tox-uv-1.25.0/.github/release.yml000066400000000000000000000001141475612564700165610ustar00rootroot00000000000000changelog: exclude: authors: - dependabot - pre-commit-ci tox-uv-1.25.0/.github/workflows/000077500000000000000000000000001475612564700164575ustar00rootroot00000000000000tox-uv-1.25.0/.github/workflows/check.yaml000066400000000000000000000026251475612564700204250ustar00rootroot00000000000000name: check on: workflow_dispatch: push: branches: ["main"] tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: env: - "3.13" - "3.12" - "3.11" - "3.10" - "3.9" - type - dev - pkg_meta steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Install tox run: uv tool install --python-preference only-managed --python 3.13 tox --with . - name: Install Python if: startsWith(matrix.env, '3.') && matrix.env != '3.13' run: uv python install --python-preference only-managed ${{ matrix.env }} - name: Setup test suite run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} env: UV_PYTHON_PREFERENCE: "only-managed" - name: Run test suite run: tox run --skip-pkg-install -e ${{ matrix.env }} env: PYTEST_ADDOPTS: "-vv --durations=20" DIFF_AGAINST: HEAD UV_PYTHON_PREFERENCE: "only-managed" tox-uv-1.25.0/.github/workflows/release.yaml000066400000000000000000000023631475612564700207670ustar00rootroot00000000000000name: Release to PyPI on: push: tags: ["*"] env: dists-artifact-name: python-package-distributions jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build package run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: ${{ env.dists-artifact-name }} path: dist/* release: needs: - build runs-on: ubuntu-latest environment: name: release url: https://pypi.org/project/tox-uv/${{ github.ref_name }} permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.12.4 with: attestations: true tox-uv-1.25.0/.gitignore000066400000000000000000000001641475612564700150530ustar00rootroot00000000000000/magic .idea *.egg-info .tox/ .coverage* coverage.xml .*_cache __pycache__ **.pyc /build dist src/tox_uv/version.py tox-uv-1.25.0/.pre-commit-config.yaml000066400000000000000000000021711475612564700173440ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.31.1 hooks: - id: check-github-workflows args: ["--verbose"] - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell additional_dependencies: ["tomli>=2.2.1"] - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.5.0" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.5.0" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.9.6" hooks: - id: ruff-format - id: ruff args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.5.1" hooks: - id: prettier args: ["--print-width=120", "--prose-wrap=always"] - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes tox-uv-1.25.0/LICENSE000066400000000000000000000017771475612564700141030ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tox-uv-1.25.0/README.md000066400000000000000000000172051475612564700143460ustar00rootroot00000000000000# tox-uv [![PyPI version](https://badge.fury.io/py/tox-uv.svg)](https://badge.fury.io/py/tox-uv) [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/tox-uv.svg)](https://pypi.python.org/pypi/tox-uv/) [![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`. - [How to use](#how-to-use) - [tox environment types provided](#tox-environment-types-provided) - [uv.lock support](#uvlock-support) - [package](#package) - [extras](#extras) - [no_default_groups](#no_default_groups) - [dependency_groups](#dependency_groups) - [uv_sync_flags](#uv_sync_flags) - [External package support](#external-package-support) - [Environment creation](#environment-creation) - [uv_seed](#uv_seed) - [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: ```bash 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 tox --runner virtualenv r -e py312 # will use virtualenv+pip ``` ## 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 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 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 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 use the `extras` config to instruct `uv` to install the specified extras, for example (this example is for the `tox.ini`, for other formats see the documentation [here](https://tox.wiki/en/latest/config.html#discovery-and-file-types)): ```ini [testenv:fix] description = run code formatter and linter (auto-fix) skip_install = true deps = pre-commit-uv>=4.1.1 commands = pre-commit run --all-files --show-diff-on-failure [testenv:type] runner = uv-venv-lock-runner description = run type checker via mypy commands = mypy {posargs:src} [testenv:dev] runner = uv-venv-lock-runner description = dev environment extras = dev test type commands = uv pip tree ``` In this example: - `fix` will use the `uv-venv-runner` and use `uv pip install` to install dependencies to the environment. - `type` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment without any extra group. - `dev` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment with the `dev`, `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 (and all others [enumerated here](https://tox.wiki/en/stable/config.html#python-run) as Python run flags). ### `package` How to install the source tree package, must be one of: - `skip`, - `wheel`, - `editable` (default), - `uv` (use uv to install the project, rather than build wheel via `tox`), - `uv-editable` (use uv to install the project in editable mode, rather than build wheel via `tox`). You should use the latter two in case you need to use any non-standard features of `uv`, such as `tool.uv.sources`. ### `extras` A list of string that selects, which extra groups you want to install with `uv sync`. By default, it is empty. ### `no_default_groups` A boolean flag to toggle installation of the `uv` [default development groups](https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups). By default, it will be `true` if the `dependency_groups` is not empty and `false` otherwise. ### `dependency_groups` Specify [PEP 735 – Dependency Groups](https://peps.python.org/pep-0735/) to install. ### `uv_sync_flags` A list of strings, containing additional flags to pass to uv sync (useful because some flags are not configurable via environment variables). For example, if you want to install the package in non editable mode and keep extra packages installed into the environment you can do: ```ini uv_sync_flags = --no-editable, --inexact ``` ### External package support 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. ## Environment creation We use `uv venv` to create virtual environments. This process can be configured with the following options: ### `uv_seed` 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. ### `uv_python_preference` 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. ### `system_site_packages` (`sitepackages`) Create virtual environments that also have access to globally installed packages. Note the default value may be overwritten by the VIRTUALENV_SYSTEM_SITE_PACKAGES environment variable. This flag works the same way as the one from [tox native virtualenv implementation](https://tox.wiki/en/latest/config.html#system_site_packages). ## 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 tox-uv-1.25.0/pyproject.toml000066400000000000000000000102201475612564700157710ustar00rootroot00000000000000[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.4", "hatchling>=1.27", ] [project] name = "tox-uv" description = "Integration of uv with tox." readme = "README.md" keywords = [ "environments", "isolated", "testing", "virtual", ] license = "MIT" maintainers = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: System", ] dynamic = [ "version", ] dependencies = [ "packaging>=24.2", "tox>=4.24.1,<5", "typing-extensions>=4.12.2; python_version<'3.10'", "uv>=0.5.31,<1", ] urls.Changelog = "https://github.com/tox-dev/tox-uv/releases" urls.Documentation = "https://github.com/tox-dev/tox-uv#tox-uv" urls.Homepage = "https://github.com/tox-dev/tox-uv" urls.Source = "https://github.com/tox-dev/tox-uv" urls.Tracker = "https://github.com/tox-dev/tox-uv/issues" entry-points.tox.tox-uv = "tox_uv.plugin" [dependency-groups] dev = [ { include-group = "lint" }, { include-group = "pkg-meta" }, { include-group = "test" }, { include-group = "type" }, ] test = [ "covdefaults>=2.3", "devpi-process>=1.0.2", "diff-cover>=9.2.2", "pytest>=8.3.4", "pytest-cov>=6", "pytest-mock>=3.14", ] type = [ "mypy==1.15", { include-group = "test" } ] lint = [ "pre-commit-uv>=4.1.4" ] pkg-meta = [ "check-wheel-contents>=0.6.1", "twine>=6.1", "uv>=0.5.31" ] [tool.hatch] build.hooks.vcs.version-file = "src/tox_uv/version.py" build.targets.sdist.include = [ "/src", "/tests", ] version.source = "vcs" [tool.ruff] line-length = 120 format.preview = true format.docstring-code-line-length = 100 format.docstring-code-format = true lint.select = [ "ALL", ] lint.ignore = [ "COM812", # Conflict with formatter "CPY", # No copyright statements "D", # no documentation for now "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible "D205", # 1 blank line required between summary line and description "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible "D301", # Use `r"""` if any backslashes in a docstring "D401", # First line of docstring should be in imperative mood "DOC201", # no support for sphinx "ISC001", # Conflict with formatter "S104", # Possible binding to all interface ] lint.per-file-ignores."tests/**/*.py" = [ "D", # don't care about documentation in tests "FBT", # don't care about booleans as positional arguments in tests "INP001", # no implicit namespace "PLC2701", # private import is fine "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S", # no safety concerns "S101", # asserts allowed in tests... ] lint.isort = { known-first-party = [ "tox_uv", "tests", ], required-imports = [ "from __future__ import annotations", ] } lint.preview = true [tool.codespell] builtin = "clear,usage,en-GB_to_en-US" write-changes = true count = true [tool.pyproject-fmt] max_supported_python = "3.13" [tool.pytest.ini_options] norecursedirs = "tests/data/*" verbosity_assertions = 2 [tool.coverage] html.show_contexts = true html.skip_covered = false paths.source = [ "src", ".tox/*/lib/*/site-packages", ".tox\\*\\Lib\\site-packages", "**/src", "**\\src", ] paths.other = [ ".", "*/tox_uv", "*\\tox_uv", ] report.omit = [ "src/tox_uv/_venv_query.py", ] report.fail_under = 100 run.parallel = true run.plugins = [ "covdefaults", ] [tool.mypy] python_version = "3.12" show_error_codes = true strict = true overrides = [ { module = [ "virtualenv.*", "uv.*", ], ignore_missing_imports = true }, ] tox-uv-1.25.0/src/000077500000000000000000000000001475612564700136515ustar00rootroot00000000000000tox-uv-1.25.0/src/tox_uv/000077500000000000000000000000001475612564700151755ustar00rootroot00000000000000tox-uv-1.25.0/src/tox_uv/__init__.py000066400000000000000000000002251475612564700173050ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations from .version import version as __version__ __all__ = [ "__version__", ] tox-uv-1.25.0/src/tox_uv/_installer.py000066400000000000000000000155201475612564700177060ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import logging from collections import defaultdict from collections.abc import Sequence from itertools import chain from typing import TYPE_CHECKING, Any, Final from packaging.requirements import Requirement from packaging.utils import parse_sdist_filename, parse_wheel_filename 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 from tox.tox_env.python.pip.req_file import PythonDeps from uv import find_uv_bin from ._package_types import UvEditablePackage, UvPackage if TYPE_CHECKING: from tox.config.main import Config from tox.tox_env.package import PathPackage from tox.tox_env.python.api import Python _LOGGER: Final[logging.Logger] = logging.getLogger(__name__) 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) def freeze_cmd(self) -> list[str]: return [self.uv, "--color", "never", "pip", "freeze"] @property def uv(self) -> str: return find_uv_bin() def _register_config(self) -> None: super()._register_config() def uv_resolution_post_process(value: str) -> str: valid_opts = {"highest", "lowest", "lowest-direct"} if value and value not in valid_opts: msg = f"Invalid value for uv_resolution: {value!r}. Valid options are: {', '.join(valid_opts)}." raise Fail(msg) return value self._env.conf.add_config( keys=["uv_resolution"], of_type=str, default="", desc="Define the resolution strategy for uv", post_process=uv_resolution_post_process, ) def default_install_command(self, conf: Config, env_name: str | None) -> Command: # noqa: ARG002 cmd = [self.uv, "pip", "install", "{opts}", "{packages}"] if self._env.options.verbosity > 3: # noqa: PLR2004 cmd.append("-v") return Command(cmd) def post_process_install_command(self, cmd: Command) -> Command: install_command = cmd.args pip_pre: bool = self._env.conf["pip_pre"] uv_resolution: str = self._env.conf["uv_resolution"] try: opts_at = install_command.index("{opts}") except ValueError: if pip_pre: install_command.extend(("--prerelease", "allow")) if uv_resolution: install_command.extend(("--resolution", uv_resolution)) else: if pip_pre: install_command[opts_at] = "--prerelease" install_command.insert(opts_at + 1, "allow") if uv_resolution: install_command[opts_at] = "--resolution" install_command.insert(opts_at + 1, uv_resolution) if not (pip_pre or uv_resolution): install_command.pop(opts_at) return cmd def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: ANN401 if isinstance(arguments, PythonDeps): self._install_requirement_file(arguments, section, of_type) elif isinstance(arguments, Sequence): # pragma: no branch self._install_list_of_deps(arguments, section, of_type) else: # pragma: no cover _LOGGER.warning("uv cannot install %r", arguments) # pragma: no cover raise SystemExit(1) # pragma: no cover def _install_list_of_deps( # noqa: C901, PLR0912 self, arguments: Sequence[ Requirement | WheelPackage | SdistPackage | EditableLegacyPackage | EditablePackage | PathPackage ], section: str, of_type: str, ) -> None: groups: dict[str, list[str]] = defaultdict(list) for arg in arguments: if isinstance(arg, Requirement): # pragma: no branch groups["req"].append(str(arg)) # pragma: no cover elif isinstance(arg, (WheelPackage, SdistPackage, EditablePackage)): groups["req"].extend(str(i) for i in arg.deps) parser = parse_sdist_filename if isinstance(arg, SdistPackage) else parse_wheel_filename name, *_ = parser(arg.path.name) groups["pkg"].append(f"{name}@{arg.path}") elif isinstance(arg, EditableLegacyPackage): groups["req"].extend(str(i) for i in arg.deps) groups["dev_pkg"].append(str(arg.path)) elif isinstance(arg, UvPackage): extras_suffix = f"[{','.join(arg.extras)}]" if arg.extras else "" groups["uv"].append(f"{arg.path}{extras_suffix}") elif isinstance(arg, UvEditablePackage): extras_suffix = f"[{','.join(arg.extras)}]" if arg.extras else "" groups["uv_editable"].append(f"{arg.path}{extras_suffix}") else: # pragma: no branch _LOGGER.warning("uv install %r", arg) # pragma: no cover raise SystemExit(1) # pragma: no cover req_of_type = f"{of_type}_deps" if groups["pkg"] or groups["dev_pkg"] else of_type for value in groups.values(): value.sort() with self._env.cache.compare(groups["req"], section, req_of_type) as (eq, old): if not eq: # pragma: no branch miss = sorted(set(old or []) - set(groups["req"])) if miss: # no way yet to know what to uninstall here (transitive dependencies?) # pragma: no branch msg = f"dependencies removed: {', '.join(str(i) for i in miss)}" # pragma: no cover raise Recreate(msg) # pragma: no branch # pragma: no cover new_deps = sorted(set(groups["req"]) - set(old or [])) if new_deps: # pragma: no branch self._execute_installer(new_deps, req_of_type) install_args = ["--reinstall"] if groups["uv"]: self._execute_installer(install_args + groups["uv"], of_type) if groups["uv_editable"]: requirements = list(chain.from_iterable(("-e", entry) for entry in groups["uv_editable"])) self._execute_installer(install_args + requirements, of_type) install_args.append("--no-deps") if groups["pkg"]: self._execute_installer(install_args + groups["pkg"], of_type) if groups["dev_pkg"]: for entry in groups["dev_pkg"]: install_args.extend(("-e", str(entry))) self._execute_installer(install_args, of_type) __all__ = [ "UvInstaller", ] tox-uv-1.25.0/src/tox_uv/_package.py000066400000000000000000000021721475612564700173030ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from tox.tox_env.python.virtual_env.package.cmd_builder import VenvCmdBuilder from tox.tox_env.python.virtual_env.package.pyproject import Pep517VenvPackager from ._package_types import UvEditablePackage, UvPackage from ._venv import UvVenv if TYPE_CHECKING: from tox.config.sets import EnvConfigSet from tox.tox_env.package import Package class UvVenvPep517Packager(Pep517VenvPackager, UvVenv): @staticmethod def id() -> str: return "uv-venv-pep-517" def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: of_type: str = for_env["package"] if of_type == UvPackage.KEY: return [UvPackage(self.core["tox_root"], for_env["extras"])] if of_type == UvEditablePackage.KEY: return [UvEditablePackage(self.core["tox_root"], for_env["extras"])] return super().perform_packaging(for_env) class UvVenvCmdBuilder(VenvCmdBuilder, UvVenv): @staticmethod def id() -> str: return "uv-venv-cmd-builder" __all__ = [ "UvVenvCmdBuilder", "UvVenvPep517Packager", ] tox-uv-1.25.0/src/tox_uv/_package_types.py000066400000000000000000000014401475612564700205240ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from tox.tox_env.python.package import PythonPathPackageWithDeps if TYPE_CHECKING: import pathlib from collections.abc import Sequence class UvBasePackage(PythonPathPackageWithDeps): """Package to be built and installed by uv directly.""" KEY: str def __init__(self, path: pathlib.Path, extras: Sequence[str]) -> None: super().__init__(path, ()) self.extras = extras class UvPackage(UvBasePackage): """Package to be built and installed by uv directly as wheel.""" KEY = "uv" class UvEditablePackage(UvBasePackage): """Package to be built and installed by uv directly as editable wheel.""" KEY = "uv-editable" __all__ = [ "UvEditablePackage", "UvPackage", ] tox-uv-1.25.0/src/tox_uv/_run.py000066400000000000000000000020361475612564700165130ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations from typing import TYPE_CHECKING from tox.tox_env.python.runner import PythonRun from ._package_types import UvEditablePackage, UvPackage from ._venv import UvVenv if TYPE_CHECKING: from pathlib import Path class UvVenvRunner(UvVenv, PythonRun): @staticmethod def id() -> str: return "uv-venv-runner" @property def _package_tox_env_type(self) -> str: return "uv-venv-pep-517" @property def _external_pkg_tox_env_type(self) -> str: return "uv-venv-cmd-builder" # pragma: no cover @property def default_pkg_type(self) -> str: tox_root: Path = self.core["tox_root"] if not (any((tox_root / i).exists() for i in ("pyproject.toml", "setup.py", "setup.cfg"))): return "skip" return super().default_pkg_type @property def _package_types(self) -> tuple[str, ...]: return *super()._package_types, UvPackage.KEY, UvEditablePackage.KEY __all__ = [ "UvVenvRunner", ] tox-uv-1.25.0/src/tox_uv/_run_lock.py000066400000000000000000000113461475612564700175270ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING, List, Literal, Set, cast # noqa: UP035 from tox.execute.request import StdinSource from tox.report import HandledError 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 ._venv import UvVenv if sys.version_info >= (3, 11): # pragma: no cover (py311+) import tomllib else: # pragma: no cover (py311+) import tomli as tomllib if TYPE_CHECKING: from tox.tox_env.package import Package class UvVenvLockRunner(UvVenv, RunToxEnv): @staticmethod def id() -> str: return "uv-venv-lock-runner" def _register_package_conf(self) -> bool: # noqa: PLR6301 return False @property def _package_tox_env_type(self) -> str: raise NotImplementedError @property def _external_pkg_tox_env_type(self) -> str: raise NotImplementedError def _build_packages(self) -> list[Package]: raise NotImplementedError def register_config(self) -> None: super().register_config() add_extras_to_env(self.conf) self.conf.add_config( keys=["dependency_groups"], of_type=Set[str], # noqa: UP006 default=set(), desc="dependency groups to install of the target package", ) self.conf.add_config( keys=["no_default_groups"], of_type=bool, default=lambda _, __: bool(self.conf["dependency_groups"]), desc="Install default groups or not", ) self.conf.add_config( keys=["uv_sync_flags"], of_type=List[str], # noqa: UP006 default=[], desc="Additional flags to pass to uv sync (for flags not configurable via environment variables)", ) self.conf.add_config( keys=["package"], of_type=Literal["editable", "wheel", "skip"], # type: ignore[arg-type] default="editable", desc="How should the package be installed", ) add_skip_missing_interpreters_to_core(self.core, self.options) def _setup_env(self) -> None: # noqa: C901 super()._setup_env() install_pkg = getattr(self.options, "install_pkg", None) if not getattr(self.options, "skip_uv_sync", False): cmd = [ "uv", "sync", "--locked", ] if self.conf["uv_python_preference"] != "none": cmd.extend(("--python-preference", self.conf["uv_python_preference"])) for extra in cast("set[str]", sorted(self.conf["extras"])): cmd.extend(("--extra", extra)) groups = sorted(self.conf["dependency_groups"]) if self.conf["no_default_groups"]: cmd.append("--no-default-groups") package = self.conf["package"] if install_pkg is not None or package == "skip": cmd.append("--no-install-project") if self.options.verbosity > 3: # noqa: PLR2004 cmd.append("-v") if package == "wheel": # need the package name here but we don't have the packaging infrastructure -> read from pyproject.toml project_file = self.core["tox_root"] / "pyproject.toml" name = None if project_file.exists(): with project_file.open("rb") as file_handler: raw = tomllib.load(file_handler) name = raw.get("project", {}).get("name") if name is None: msg = "Could not detect project name" raise HandledError(msg) cmd.extend(("--no-editable", "--reinstall-package", name)) for group in groups: cmd.extend(("--group", group)) cmd.extend(self.conf["uv_sync_flags"]) cmd.extend(("-p", self.env_version_spec())) show = self.options.verbosity > 2 # noqa: PLR2004 outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="uv-sync", show=show) 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]: env = super().environment_variables env["UV_PROJECT_ENVIRONMENT"] = str(self.venv_dir) return env __all__ = [ "UvVenvLockRunner", ] tox-uv-1.25.0/src/tox_uv/_venv.py000066400000000000000000000276331475612564700166770ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import json import logging import os import sys from abc import ABC from functools import cached_property from importlib.resources import as_file, files from pathlib import Path from platform import python_implementation from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Type, cast # noqa: UP035 from tox.config.loader.str_convert import StrConvert from tox.execute.local_sub_process import LocalSubProcessExecutor from tox.execute.request import StdinSource from tox.tox_env.errors import Skip from tox.tox_env.python.api import Python, PythonInfo, VersionInfo from tox.tox_env.python.virtual_env.api import VirtualEnv from uv import find_uv_bin from virtualenv.discovery.py_spec import PythonSpec from ._installer import UvInstaller if sys.version_info >= (3, 10): # pragma: no cover (py310+) from typing import TypeAlias else: # pragma: no cover ( None: self._executor: Execute | None = None self._installer: UvInstaller | None = None self._created = False self._displayed_uv_constraint_warning = False super().__init__(create_args) def register_config(self) -> None: super().register_config() self.conf.add_config( keys=["uv_seed"], of_type=bool, default=False, desc="add seed packages to the created venv", ) self.conf.add_config( keys=["system_site_packages", "sitepackages"], of_type=bool, default=lambda conf, name: StrConvert().to_bool( # noqa: ARG005 self.environment_variables.get("VIRTUALENV_SYSTEM_SITE_PACKAGES", "False"), ), desc="create virtual environments that also have access to globally installed packages.", ) # The cast(...) might seems superfluous but removing it makes mypy crash. The problem isy on tox typing side. self.conf.add_config( keys=["uv_python_preference"], of_type=cast("Type[Optional[PythonPreference]]", Optional[PythonPreference]), # type: ignore[valid-type] # noqa: UP006 # use os.environ here instead of self.environment_variables as this value is needed to create the virtual # environment, if environment variables use env_site_packages_dir we would run into a chicken-egg problem. default=lambda conf, name: os.environ.get("UV_PYTHON_PREFERENCE", "system"), # noqa: ARG005 desc=( "Whether to prefer using Python installations that are already" " present on the system, or those that are downloaded and" " installed by uv [possible values: none, only-managed, installed," " managed, system, only-system]. Use none to use uv's" " default. Our default value is 'system', while uv's default" " value is 'managed' because we prefer using same python" " interpreters with all tox environments and avoid accidental" " downloading of other interpreters." ), ) def python_cache(self) -> dict[str, Any]: result = super().python_cache() result["seed"] = self.conf["uv_seed"] if self.conf["uv_python_preference"] != "none": result["python_preference"] = self.conf["uv_python_preference"] result["venv"] = str(self.venv_dir.relative_to(self.env_dir)) return result @property def executor(self) -> Execute: if self._executor is None: self._executor = LocalSubProcessExecutor(self.options.is_colored) return self._executor @property def installer(self) -> Installer[Any]: if self._installer is None: self._installer = UvInstaller(self) return self._installer @property def runs_on_platform(self) -> str: return sys.platform def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR6301 for base in base_python: # pragma: no branch if base == sys.executable: version_info = sys.version_info return PythonInfo( implementation=python_implementation(), version_info=VersionInfo( major=version_info.major, minor=version_info.minor, micro=version_info.micro, releaselevel=version_info.releaselevel, serial=version_info.serial, ), version=sys.version, is_64=sys.maxsize > 2**32, platform=sys.platform, extra={}, ) base_path = Path(base) if base_path.is_absolute(): info = VirtualEnv.get_virtualenv_py_info(base_path) return PythonInfo( implementation=info.implementation, version_info=VersionInfo(*info.version_info), version=info.version, is_64=info.architecture == 64, # noqa: PLR2004 platform=info.platform, extra={"executable": base}, ) spec = PythonSpec.from_string_spec(base) return PythonInfo( implementation=spec.implementation or "CPython", version_info=VersionInfo( major=spec.major, minor=spec.minor, micro=spec.micro, releaselevel="", serial=0, ), version=str(spec), is_64=spec.architecture == 64, # noqa: PLR2004 platform=sys.platform, extra={"architecture": spec.architecture}, ) return None # pragma: no cover @classmethod def python_spec_for_path(cls, path: Path) -> PythonSpec: """ Get the spec for an absolute path to a Python executable. :param path: the path investigated :return: the found spec """ return VirtualEnv.python_spec_for_path(path) @property def uv(self) -> str: return find_uv_bin() @property def venv_dir(self) -> Path: return cast("Path", self.conf["env_dir"]) @property def environment_variables(self) -> dict[str, str]: env = super().environment_variables env.pop("UV_PYTHON", None) # UV_PYTHON takes precedence over VIRTUAL_ENV env["VIRTUAL_ENV"] = str(self.venv_dir) if "UV_CONSTRAINT" not in env and not self._displayed_uv_constraint_warning: for pip_var in ("PIP_CONSTRAINT", "PIP_CONSTRAINTS"): if pip_var in env: _LOGGER.warning( "Found %s defined, you may want to also define UV_CONSTRAINT to match pip behavior.", pip_var ) self._displayed_uv_constraint_warning = True break return env def _default_pass_env(self) -> list[str]: env = super()._default_pass_env() env.append("UV_*") # accept uv env vars if sys.platform == "darwin": # pragma: darwin cover env.append("MACOSX_DEPLOYMENT_TARGET") # needed for macOS binary builds env.append("PKG_CONFIG_PATH") # needed for binary builds return env def create_python_env(self) -> None: version_spec = self.env_version_spec() cmd: list[str] = [self.uv, "venv", "-p", version_spec, "--allow-existing"] if self.options.verbosity > 3: # noqa: PLR2004 cmd.append("-v") if self.conf["uv_seed"]: cmd.append("--seed") if self.conf["system_site_packages"]: cmd.append("--system-site-packages") if self.conf["uv_python_preference"] != "none": cmd.extend(["--python-preference", self.conf["uv_python_preference"]]) cmd.append(str(self.venv_dir)) outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="venv", show=None) if self.core["skip_missing_interpreters"] and outcome.exit_code == 1: msg = f"could not find python interpreter with spec(s): {version_spec}" raise Skip(msg) outcome.assert_success() self._created = True @property def _allow_externals(self) -> list[str]: result = super()._allow_externals result.append(self.uv) return result def prepend_env_var_path(self) -> list[Path]: return [self.env_bin_dir(), Path(self.uv).parent] def env_bin_dir(self) -> Path: if sys.platform == "win32": # pragma: win32 cover return self.venv_dir / "Scripts" else: # pragma: win32 no cover # noqa: RET505 return self.venv_dir / "bin" def env_python(self) -> Path: suffix = ".exe" if sys.platform == "win32" else "" return self.env_bin_dir() / f"python{suffix}" def env_site_package_dir(self) -> Path: if sys.platform == "win32": # pragma: win32 cover return self.venv_dir / "Lib" / "site-packages" # pragma: win32 no cover py = self._py_info impl = "pypy" if py.implementation == "pypy" else "python" return self.venv_dir / "lib" / f"{impl}{py.version_dot}" / "site-packages" def env_version_spec(self) -> str: base = self.base_python.version_info imp = self.base_python.impl_lower executable = self.base_python.extra.get("executable") architecture = self.base_python.extra.get("architecture") if executable: version_spec = str(executable) elif ( architecture is None and (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp) ): version_spec = sys.executable else: uv_imp = imp or "" if not base.major: version_spec = f"{uv_imp}" elif not base.minor: version_spec = f"{uv_imp}{base.major}" elif architecture is not None and self.base_python.platform == "win32": uv_arch = {32: "x86", 64: "x86_64"}[architecture] version_spec = f"{uv_imp}-{base.major}.{base.minor}-windows-{uv_arch}-none" else: version_spec = f"{uv_imp}{base.major}.{base.minor}" return version_spec @cached_property def _py_info(self) -> PythonInfo: # pragma: win32 no cover if not self._created and not self.env_python().exists(): # called during config, no environment setup self.create_python_env() if not self._paths: self._paths = self.prepend_env_var_path() with as_file(files("tox_uv") / "_venv_query.py") as filename: cmd = [str(self.env_python()), str(filename)] outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="venv-query", show=False) outcome.assert_success() res = json.loads(outcome.out) return PythonInfo( implementation=res["implementation"], version_info=VersionInfo( major=res["version_info"][0], minor=res["version_info"][1], micro=res["version_info"][2], releaselevel=res["version_info"][3], serial=res["version_info"][4], ), version=res["version"], is_64=res["is_64"], platform=sys.platform, extra={}, ) __all__ = [ "UvVenv", ] tox-uv-1.25.0/src/tox_uv/_venv_query.py000066400000000000000000000005011475612564700201050ustar00rootroot00000000000000from __future__ import annotations import json import sys from platform import python_implementation print( # noqa: T201 json.dumps({ "implementation": python_implementation().lower(), "version_info": sys.version_info, "version": sys.version, "is_64": sys.maxsize > 2**32, }) ) tox-uv-1.25.0/src/tox_uv/plugin.py000066400000000000000000000021761475612564700170530ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations from importlib.metadata import version from typing import TYPE_CHECKING from tox.plugin import impl from ._package import UvVenvCmdBuilder, UvVenvPep517Packager from ._run import UvVenvRunner from ._run_lock import UvVenvLockRunner if TYPE_CHECKING: from tox.config.cli.parser import ToxParser from tox.tox_env.register import ToxEnvRegister @impl def tox_register_tox_env(register: ToxEnvRegister) -> None: register.add_run_env(UvVenvRunner) register.add_run_env(UvVenvLockRunner) register.add_package_env(UvVenvPep517Packager) register.add_package_env(UvVenvCmdBuilder) register._default_run_env = UvVenvRunner.id() # noqa: SLF001 @impl def tox_add_option(parser: ToxParser) -> None: for key in ("run", "exec"): parser.handlers[key][0].add_argument( "--skip-uv-sync", dest="skip_uv_sync", help="skip uv sync (lock mode only)", action="store_true", ) def tox_append_version_info() -> str: return f"with uv=={version('uv')}" __all__ = [ "tox_register_tox_env", ] tox-uv-1.25.0/src/tox_uv/py.typed000066400000000000000000000000001475612564700166620ustar00rootroot00000000000000tox-uv-1.25.0/tests/000077500000000000000000000000001475612564700142245ustar00rootroot00000000000000tox-uv-1.25.0/tests/conftest.py000066400000000000000000000017001475612564700164210ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING from unittest import mock import pytest if TYPE_CHECKING: from collections.abc import Generator @pytest.fixture(autouse=True) def mock_settings_env_vars() -> Generator[None, None, None]: """Isolated testing from user's environment.""" with mock.patch.dict(os.environ, {"TOX_USER_CONFIG_FILE": os.devnull}): yield @pytest.fixture(scope="session") def root() -> Path: return Path(__file__).parent @pytest.fixture(scope="session") def demo_pkg_setuptools(root: Path) -> Path: return root / "demo_pkg_setuptools" @pytest.fixture(scope="session") def demo_pkg_inline(root: Path) -> Path: return root / "demo_pkg_inline" @pytest.fixture def clear_python_preference_env_var(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) pytest_plugins = [ "tox.pytest", ] tox-uv-1.25.0/tests/demo_pkg_inline/000077500000000000000000000000001475612564700173475ustar00rootroot00000000000000tox-uv-1.25.0/tests/demo_pkg_inline/build.py000066400000000000000000000103041475612564700210160ustar00rootroot00000000000000from __future__ import annotations import os import re import sys import tarfile from pathlib import Path from textwrap import dedent from zipfile import ZipFile name = "demo-pkg-inline" if os.environ.get("WITH_DASH") else "demo_pkg_inline" name_in_artifact = re.sub(r"[^\w\d.]+", "_", name, flags=re.UNICODE) # per PEP-427 version = "1.0.0" dist_info = f"{name_in_artifact}-{version}.dist-info" module = name_in_artifact logic = f"{module}/__init__.py" plugin = f"{module}/example_plugin.py" entry_points = f"{dist_info}/entry_points.txt" metadata = f"{dist_info}/METADATA" wheel = f"{dist_info}/WHEEL" record = f"{dist_info}/RECORD" content = { logic: f"def do():\n print('greetings from {name}')", plugin: """ try: from tox.plugin import impl from tox.tox_env.python.virtual_env.runner import VirtualEnvRunner from tox.tox_env.register import ToxEnvRegister except ImportError: pass else: class ExampleVirtualEnvRunner(VirtualEnvRunner): @staticmethod def id() -> str: return "example" @impl def tox_register_tox_env(register: ToxEnvRegister) -> None: register.add_run_env(ExampleVirtualEnvRunner) """, } metadata_files = { entry_points: f""" [tox] example = {module}.example_plugin""", metadata: f""" Metadata-Version: 2.1 Name: {name} Version: {version} Summary: UNKNOWN Home-page: UNKNOWN Author: UNKNOWN Author-email: UNKNOWN License: UNKNOWN Platform: UNKNOWN UNKNOWN """, wheel: f""" Wheel-Version: 1.0 Generator: {name}-{version} Root-Is-Purelib: true Tag: py{sys.version_info[0]}-none-any """, f"{dist_info}/top_level.txt": module, record: f""" {module}/__init__.py,, {dist_info}/METADATA,, {dist_info}/WHEEL,, {dist_info}/top_level.txt,, {dist_info}/RECORD,, """, } def build_wheel( wheel_directory: str, config_settings: dict[str, str] | None = None, # noqa: ARG001 metadata_directory: str | None = None, ) -> str: base_name = f"{name_in_artifact}-{version}-py{sys.version_info[0]}-none-any.whl" path = Path(wheel_directory) / base_name with ZipFile(str(path), "w") as zip_file_handler: for arc_name, data in content.items(): # pragma: no branch zip_file_handler.writestr(arc_name, dedent(data).strip()) if metadata_directory is not None: for sub_directory, _, filenames in os.walk(metadata_directory): for filename in filenames: src = str(Path(metadata_directory) / sub_directory / filename) dest = str(Path(sub_directory) / filename) zip_file_handler.write(src, dest) else: for arc_name, data in metadata_files.items(): zip_file_handler.writestr(arc_name, dedent(data).strip()) print(f"created wheel {path}") # noqa: T201 return base_name def get_requires_for_build_wheel(config_settings: dict[str, str] | None = None) -> list[str]: # noqa: ARG001 return [] # pragma: no cover # only executed in non-host pythons def build_editable( wheel_directory: str, config_settings: dict[str, str] | None = None, metadata_directory: str | None = None, ) -> str: return build_wheel(wheel_directory, config_settings, metadata_directory) def build_sdist(sdist_directory: str, config_settings: dict[str, str] | None = None) -> str: # noqa: ARG001 result = f"{name_in_artifact}-{version}.tar.gz" # pragma: win32 cover with tarfile.open(str(Path(sdist_directory) / result), "w:gz") as tar: # pragma: win32 cover root = Path(__file__).parent # pragma: win32 cover tar.add(str(root / "build.py"), "build.py") # pragma: win32 cover tar.add(str(root / "pyproject.toml"), "pyproject.toml") # pragma: win32 cover return result # pragma: win32 cover def get_requires_for_build_sdist(config_settings: dict[str, str] | None = None) -> list[str]: # noqa: ARG001 return [] # pragma: no cover # only executed in non-host pythons tox-uv-1.25.0/tests/demo_pkg_inline/pyproject.toml000066400000000000000000000001601475612564700222600ustar00rootroot00000000000000[build-system] build-backend = "build" requires = [ ] backend-path = [ ".", ] [tool.black] line-length = 120 tox-uv-1.25.0/tests/demo_pkg_setuptools/000077500000000000000000000000001475612564700203125ustar00rootroot00000000000000tox-uv-1.25.0/tests/demo_pkg_setuptools/demo_pkg_setuptools/000077500000000000000000000000001475612564700244005ustar00rootroot00000000000000tox-uv-1.25.0/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py000066400000000000000000000001651475612564700265130ustar00rootroot00000000000000from __future__ import annotations def do() -> None: print("greetings from demo_pkg_setuptools") # noqa: T201 tox-uv-1.25.0/tests/demo_pkg_setuptools/pyproject.toml000066400000000000000000000001321475612564700232220ustar00rootroot00000000000000[build-system] build-backend = 'setuptools.build_meta' requires = [ "setuptools>=63", ] tox-uv-1.25.0/tests/demo_pkg_setuptools/setup.cfg000066400000000000000000000001221475612564700221260ustar00rootroot00000000000000[metadata] name = demo_pkg_setuptools version = 1.2.3 [options] packages = find: tox-uv-1.25.0/tests/test_tox_uv_api.py000066400000000000000000000011601475612564700200100ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from tox.execute import ExecuteRequest from tox.pytest import ToxProjectCreator def test_uv_list_dependencies_command(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip"}) execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = project.run("--list-dependencies", "-vv") result.assert_success() request: ExecuteRequest = execute_calls.call_args[0][3] assert request.cmd[1:] == ["--color", "never", "pip", "freeze"] tox-uv-1.25.0/tests/test_tox_uv_installer.py000066400000000000000000000066031475612564700212430ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from tox.pytest import ToxProjectCreator def test_uv_install_in_ci_list(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("CI", "1") project = tox_project({"tox.ini": "[testenv]\ndeps = tomli\npackage=skip"}) result = project.run() result.assert_success() report = {i.split("=")[0] for i in result.out.splitlines()[-3][4:].split(",")} assert report == {"tomli"} def test_uv_install_in_ci_seed(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("CI", "1") project = tox_project({"tox.ini": "[testenv]\npackage=skip\nuv_seed = true"}) result = project.run() result.assert_success() report = {i.split("=")[0] for i in result.out.splitlines()[-3][4:].split(",")} if sys.version_info >= (3, 12): # pragma: >=3.12 cover assert report == {"pip"} else: # pragma: <3.12 cover assert report == {"pip", "setuptools", "wheel"} def test_uv_install_with_pre(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\ndeps = tomli\npip_pre = true\npackage=skip"}) result = project.run("-vv") result.assert_success() def test_uv_install_with_pre_custom_install_cmd(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = tomli pip_pre = true package = skip install_command = uv pip install {packages} """ }) result = project.run("-vv") result.assert_success() def test_uv_install_without_pre_custom_install_cmd(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = tomli package = skip install_command = uv pip install {packages} """ }) result = project.run("-vv") result.assert_success() @pytest.mark.parametrize("strategy", ["highest", "lowest", "lowest-direct"]) def test_uv_install_with_resolution_strategy(tox_project: ToxProjectCreator, strategy: str) -> None: project = tox_project({"tox.ini": f"[testenv]\ndeps = tomli>=2.0.1\npackage = skip\nuv_resolution = {strategy}"}) execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = project.run("-vv") result.assert_success() assert execute_calls.call_args[0][3].cmd[2:] == ["install", "--resolution", strategy, "tomli>=2.0.1", "-v"] def test_uv_install_with_invalid_resolution_strategy(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\ndeps = tomli>=2.0.1\npackage = skip\nuv_resolution = invalid"}) result = project.run("-vv") result.assert_failed(code=1) assert "Invalid value for uv_resolution: 'invalid'." in result.out def test_uv_install_with_resolution_strategy_custom_install_cmd(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = tomli>=2.0.1 package = skip uv_resolution = lowest-direct install_command = uv pip install {packages} """ }) execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = project.run("-vv") result.assert_success() assert execute_calls.call_args[0][3].cmd[2:] == ["install", "tomli>=2.0.1", "--resolution", "lowest-direct"] tox-uv-1.25.0/tests/test_tox_uv_lock.py000066400000000000000000000404041475612564700201730ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING import pytest from uv import find_uv_bin if TYPE_CHECKING: from tox.pytest import ToxProjectCreator @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_lock_list_dependencies_command(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] runner = uv-venv-lock-runner extras = type dev commands = python hello """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("--list-dependencies", "-vv") 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--extra", "dev", "--extra", "type", "-v", "-p", sys.executable, ], ), ("py", "freeze", [uv, "--color", "never", "pip", "freeze"]), ("py", "commands[0]", ["python", "hello"]), ] assert len(calls) == len(expected) for i in range(len(calls)): assert calls[i] == expected[i] @pytest.mark.usefixtures("clear_python_preference_env_var") @pytest.mark.parametrize("verbose", ["", "-v", "-vv", "-vvv"]) def test_uv_lock_command(tox_project: ToxProjectCreator, verbose: str) -> None: project = tox_project({ "tox.ini": """ [testenv] runner = uv-venv-lock-runner extras = type dev commands = python hello """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run(*[verbose] if verbose else []) 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() v_args = ["-v"] if verbose not in {"", "-v"} else [] expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", *v_args, "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--extra", "dev", "--extra", "type", *v_args, "-p", sys.executable, ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected show_uv_output = execute_calls.call_args_list[1].args[4] assert show_uv_output is (bool(verbose)) @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_lock_with_default_groups(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] runner = uv-venv-lock-runner no_default_groups = False """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("-vv") 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ("py", "uv-sync", ["uv", "sync", "--locked", "--python-preference", "system", "-v", "-p", sys.executable]), ] assert calls == expected @pytest.mark.usefixtures("clear_python_preference_env_var") @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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--no-install-project", "-v", "-p", sys.executable, ], ), ( "py", "install_external", [uv, "pip", "install", "--reinstall", "--no-deps", f"tox-uv@{wheel}", "-v"], ), ] assert calls == expected @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_sync_extra_flags(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] runner = uv-venv-lock-runner no_default_groups = false uv_sync_flags = --no-editable, --inexact commands = python hello """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run() 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--no-editable", "--inexact", "-p", sys.executable, ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_sync_extra_flags_toml(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.toml": """ [env_run_base] runner = "uv-venv-lock-runner" no_default_groups = false uv_sync_flags = ["--no-editable", "--inexact"] commands = [["python", "hello"]] """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run() 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--no-editable", "--inexact", "-p", sys.executable, ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_sync_dependency_groups(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.toml": """ [env_run_base] runner = "uv-venv-lock-runner" with_dev = true dependency_groups = ["test", "type"] commands = [["python", "hello"]] """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run() 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--no-default-groups", "--group", "test", "--group", "type", "-p", sys.executable, ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected @pytest.mark.parametrize( ("uv_python_preference", "injected"), [ pytest.param("none", [], id="on"), pytest.param("system", ["--python-preference", "system"], id="off"), ], ) def test_uv_sync_uv_python_preference( tox_project: ToxProjectCreator, uv_python_preference: str, injected: list[str] ) -> None: project = tox_project({ "tox.toml": f""" [env_run_base] runner = "uv-venv-lock-runner" with_dev = true dependency_groups = ["test", "type"] commands = [["python", "hello"]] uv_python_preference = "{uv_python_preference}" """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run() 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", *injected, str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", *injected, "--no-default-groups", "--group", "test", "--group", "type", "-p", sys.executable, ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected def test_skip_uv_sync(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) project = tox_project({ "tox.toml": """ [env_run_base] runner = "uv-venv-lock-runner" commands = [["python", "hello"]] """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("run", "--skip-uv-sync") 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected def test_uv_package_wheel(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) project = tox_project({ "tox.toml": """ [env_run_base] runner = "uv-venv-lock-runner" package = "wheel" """, "pyproject.toml": """ [project] name = "demo" """, }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("run", "--notest") 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--no-editable", "--reinstall-package", "demo", "-p", sys.executable, ], ), ] assert calls == expected def test_uv_package_wheel_no_pyproject(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) project = tox_project({ "tox.toml": """ [env_run_base] runner = "uv-venv-lock-runner" package = "wheel" """, }) project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("run", "--notest") result.assert_failed() def test_skip_uv_package_skip(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) project = tox_project({ "tox.toml": """ [env_run_base] runner = "uv-venv-lock-runner" package = "skip" """ }) execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("run", "--notest") 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", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ "uv", "sync", "--locked", "--python-preference", "system", "--no-install-project", "-p", sys.executable, ], ), ] assert calls == expected tox-uv-1.25.0/tests/test_tox_uv_package.py000066400000000000000000000034141475612564700206360ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from pathlib import Path from tox.pytest import ToxProjectCreator def test_uv_package_skip(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip"}) result = project.run("-vv") result.assert_success() def test_uv_package_use_default_from_file(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip", "pyproject.toml": ""}) result = project.run("-vv") result.assert_success() @pytest.mark.parametrize("with_dash", [True, False], ids=["name_dash", "name_underscore"]) @pytest.mark.parametrize("package", ["sdist", "wheel", "editable", "uv", "uv-editable"]) def test_uv_package_artifact( tox_project: ToxProjectCreator, package: str, demo_pkg_inline: Path, with_dash: bool ) -> None: ini = f"[testenv]\npackage={package}" if with_dash: ini += "\n[testenv:.pkg]\nset_env = WITH_DASH = 1" project = tox_project({"tox.ini": ini}, base=demo_pkg_inline) result = project.run() result.assert_success() def test_uv_package_editable_legacy(tox_project: ToxProjectCreator, demo_pkg_setuptools: Path) -> None: ini = f""" [testenv] package=editable-legacy [testenv:.pkg] uv_seed = true {"deps = wheel" if sys.version_info >= (3, 12) else ""} """ project = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools) result = project.run() result.assert_success() def test_uv_package_requirements(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\ndeps=-r demo.txt", "demo.txt": "tomli"}) result = project.run("-vv") result.assert_success() tox-uv-1.25.0/tests/test_tox_uv_venv.py000066400000000000000000000441051475612564700202230ustar00rootroot00000000000000from __future__ import annotations import importlib.util import os import os.path import pathlib import platform import subprocess import sys from configparser import ConfigParser from importlib.metadata import version from typing import TYPE_CHECKING, get_args from unittest import mock import pytest import tox.tox_env.errors from tox.tox_env.python.api import PythonInfo, VersionInfo from tox_uv._venv import PythonPreference, UvVenv if TYPE_CHECKING: from tox.pytest import ToxProjectCreator def test_uv_venv_self(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip"}) result = project.run("-vv") result.assert_success() def test_uv_venv_pass_env(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip"}) result = project.run("c", "-k", "pass_env") result.assert_success() parser = ConfigParser() parser.read_string(result.out) pass_through = set(parser["testenv:py"]["pass_env"].splitlines()) if sys.platform == "darwin": # pragma: darwin cover assert "MACOSX_DEPLOYMENT_TARGET" in pass_through assert "UV_*" in pass_through assert "PKG_CONFIG_PATH" in pass_through @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_venv_preference_system_by_default(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]"}) result = project.run("c", "-k", "uv_python_preference") result.assert_success() parser = ConfigParser() parser.read_string(result.out) got = parser["testenv:py"]["uv_python_preference"] assert got == "system" def test_uv_venv_preference_override_via_env_var( tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch ) -> None: project = tox_project({"tox.ini": "[testenv]"}) monkeypatch.setenv("UV_PYTHON_PREFERENCE", "only-managed") result = project.run("c", "-k", "uv_python_preference") result.assert_success() parser = ConfigParser() parser.read_string(result.out) got = parser["testenv:py"]["uv_python_preference"] assert got == "only-managed" def test_uv_venv_preference_override_via_env_var_and_set_env_depends_on_py( tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch ) -> None: project = tox_project({"tox.ini": "[testenv]\nset_env=A={env_site_packages_dir}"}) monkeypatch.setenv("UV_PYTHON_PREFERENCE", "only-managed") result = project.run("c", "-k", "set_env") result.assert_success() assert str(project.path) in result.out def test_uv_venv_spec(tox_project: ToxProjectCreator) -> None: ver = sys.version_info project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={ver.major}.{ver.minor}"}) result = project.run("-vv") result.assert_success() def test_uv_venv_spec_major_only(tox_project: ToxProjectCreator) -> None: ver = sys.version_info project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={ver.major}"}) result = project.run("-vv") result.assert_success() @pytest.mark.parametrize( ("pypy", "expected_uv_pypy"), [ ("pypy", "pypy"), ("pypy9", "pypy9"), ("pypy999", "pypy9.99"), ("pypy9.99", "pypy9.99"), ], ) def test_uv_venv_spec_pypy( capfd: pytest.CaptureFixture[str], tox_project: ToxProjectCreator, pypy: str, expected_uv_pypy: str, ) -> None: """Validate that major and minor versions are correctly applied to implementations. This test prevents a regression that occurred when the testenv name was "pypy": the uv runner was asked to use "pypyNone" as the Python version. The test is dependent on what PyPy interpreters are installed on the system; if any PyPy is available then the "pypy" value will not raise a Skip exception, and STDOUT will be captured in `result.out`. However, it is expected that no system will have PyPy v9.x installed, so STDOUT must be read from `capfd` after the Skip exception is caught. Since it is unknown whether any PyPy interpreter will be installed, the `else` block's branch coverage is disabled. """ project = tox_project({"tox.ini": f"[tox]\nenv_list = {pypy}"}) try: result = project.run("config", "-vv") except tox.tox_env.errors.Skip: stdout, _ = capfd.readouterr() else: # pragma: no cover (PyPy might not be available on the system) stdout = result.out assert "pypyNone" not in stdout assert f"-p {expected_uv_pypy} " in stdout @pytest.mark.parametrize( ("implementation", "expected_implementation", "expected_name"), [ ("", "cpython", "cpython"), ("py", "cpython", "cpython"), ("pypy", "pypy", "pypy"), ], ) def test_uv_venv_spec_full_implementation( tox_project: ToxProjectCreator, implementation: str, expected_implementation: str, expected_name: str, ) -> None: """Validate that Python implementations are explicitly passed to uv's `-p` argument. This test ensures that uv searches for the target Python implementation and version, even if another implementation -- with the same language version -- is found on the path first. This prevents a regression to a bug that occurred when PyPy 3.10 was on the PATH and tox was invoked with `tox -e py3.10`: uv was invoked with `-p 3.10` and found PyPy 3.10, not CPython 3.10. """ project = tox_project({}) result = project.run("run", "-vve", f"{implementation}9.99") # Verify that uv was invoked with the full Python implementation and version. assert f" -p {expected_implementation}9.99 " in result.out # Verify that uv interpreted the `-p` argument as a Python spec, not an executable. # This confirms that tox-uv is passing recognizable, usable `-p` arguments to uv. assert f"no interpreter found for {expected_name} 9.99" in result.err.lower() def test_uv_venv_system_site_packages(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\nsystem_site_packages=true"}) result = project.run("-vv") result.assert_success() @pytest.fixture def other_interpreter_exe() -> pathlib.Path: # pragma: no cover """Returns an interpreter executable path that is not the exact same as `sys.executable`. Necessary because `sys.executable` gets short-circuited when used as `base_python`.""" exe = pathlib.Path(sys.executable) base_python: pathlib.Path | None = None if exe.name in {"python", "python3"}: # python -> pythonX.Y ver = sys.version_info base_python = exe.with_name(f"python{ver.major}.{ver.minor}") elif exe.name[-1].isdigit(): # python X.Y -> python base_python = exe.with_name(exe.stem[:-1]) elif exe.suffix == ".exe": # python.exe <-> pythonw.exe base_python = ( exe.with_name(exe.stem[:-1] + ".exe") if exe.stem.endswith("w") else exe.with_name(exe.stem + "w.exe") ) if not base_python or not base_python.is_file(): pytest.fail("Tried to pick a base_python that is not sys.executable, but failed.") return base_python def test_uv_venv_spec_abs_path(tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path) -> None: project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={other_interpreter_exe}"}) result = project.run("-vv") result.assert_success() def test_uv_venv_spec_abs_path_conflict_ver( tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path ) -> None: # py27 is long gone, but still matches the testenv capture regex, so we know it will fail project = tox_project({"tox.ini": f"[testenv:py27]\npackage=skip\nbase_python={other_interpreter_exe}"}) result = project.run("-vv", "-e", "py27") result.assert_failed() assert f"failed with env name py27 conflicting with base python {other_interpreter_exe}" in result.out def test_uv_venv_spec_abs_path_conflict_impl( tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path ) -> None: env = "pypy" if platform.python_implementation() == "CPython" else "cpython" project = tox_project({"tox.ini": f"[testenv:{env}]\npackage=skip\nbase_python={other_interpreter_exe}"}) result = project.run("-vv", "-e", env) result.assert_failed() assert f"failed with env name {env} conflicting with base python {other_interpreter_exe}" in result.out def test_uv_venv_na(tox_project: ToxProjectCreator) -> None: # skip_missing_interpreters is true by default project = tox_project({"tox.ini": "[testenv]\npackage=skip\nbase_python=1.0"}) result = project.run("-vv") # When a Python interpreter is missing in a pytest environment, project.run # return code is equal to -1 result.assert_failed(code=-1) def test_uv_venv_skip_missing_interpreters_fail(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": "[tox]\nskip_missing_interpreters=false\n[testenv]\npackage=skip\nbase_python=1.0" }) result = project.run("-vv") result.assert_failed(code=1) def test_uv_venv_skip_missing_interpreters_pass(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": "[tox]\nskip_missing_interpreters=true\n[testenv]\npackage=skip\nbase_python=1.0" }) result = project.run("-vv") # When a Python interpreter is missing in a pytest environment, project.run # return code is equal to -1 result.assert_failed(code=-1) def test_uv_venv_platform_check(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": f"[testenv]\nplatform={sys.platform}\npackage=skip"}) result = project.run("-vv") result.assert_success() def test_uv_env_bin_dir(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{env_bin_dir}\")'"}) result = project.run("-vv") result.assert_success() env_bin_dir = str(project.path / ".tox" / "py" / ("Scripts" if sys.platform == "win32" else "bin")) assert env_bin_dir in result.out def test_uv_env_has_access_to_plugin_uv(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=uv --version"}) result = project.run() result.assert_success() ver = version("uv") assert f"uv {ver}" in result.out def test_uv_env_python(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{env_python}\")'"}) result = project.run("-vv") result.assert_success() exe = "python.exe" if sys.platform == "win32" else "python" env_bin_dir = str(project.path / ".tox" / "py" / ("Scripts" if sys.platform == "win32" else "bin") / exe) assert env_bin_dir in result.out @pytest.mark.parametrize( "preference", get_args(PythonPreference), ) def test_uv_env_python_preference( tox_project: ToxProjectCreator, *, preference: str, ) -> None: project = tox_project({ "tox.ini": ( "[testenv]\n" "package=skip\n" f"uv_python_preference={preference}\n" "commands=python -c 'print(\"{env_python}\")'" ) }) result = project.run("-vv") result.assert_success() exe = "python.exe" if sys.platform == "win32" else "python" env_bin_dir = str(project.path / ".tox" / "py" / ("Scripts" if sys.platform == "win32" else "bin") / exe) assert env_bin_dir in result.out def test_uv_env_site_package_dir_run(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{envsitepackagesdir}\")'"}) result = project.run("-vv") result.assert_success() env_dir = project.path / ".tox" / "py" ver = sys.version_info if sys.platform == "win32": # pragma: win32 cover path = str(env_dir / "Lib" / "site-packages") else: # pragma: win32 no cover impl = "pypy" if sys.implementation.name.lower() == "pypy" else "python" path = str(env_dir / "lib" / f"{impl}{ver.major}.{ver.minor}" / "site-packages") assert path in result.out def test_uv_env_site_package_dir_conf(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands={envsitepackagesdir}"}) result = project.run("c", "-e", "py", "-k", "commands") result.assert_success() env_dir = project.path / ".tox" / "py" ver = sys.version_info if sys.platform == "win32": # pragma: win32 cover path = str(env_dir / "Lib" / "site-packages") else: # pragma: win32 no cover impl = "pypy" if sys.implementation.name.lower() == "pypy" else "python" path = str(env_dir / "lib" / f"{impl}{ver.major}.{ver.minor}" / "site-packages") assert path in result.out def test_uv_env_python_not_in_path(tox_project: ToxProjectCreator) -> None: # Make sure there is no pythonX.Y in the search path ver = sys.version_info exe_ext = ".exe" if sys.platform == "win32" else "" python_exe = f"python{ver.major}.{ver.minor}{exe_ext}" env = dict(os.environ) env["PATH"] = os.path.pathsep.join( path for path in env["PATH"].split(os.path.pathsep) if not (pathlib.Path(path) / python_exe).is_file() ) # Make sure the Python interpreter can find our Tox module tox_spec = importlib.util.find_spec("tox") assert tox_spec is not None tox_lines = subprocess.check_output( [sys.executable, "-c", "import tox; print(tox.__file__);"], encoding="UTF-8", env=env ).splitlines() assert tox_lines == [tox_spec.origin] # Now use that Python interpreter to run Tox project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{env_python}\")'"}) tox_ini = project.path / "tox.ini" assert tox_ini.is_file() subprocess.check_call([sys.executable, "-m", "tox", "-c", tox_ini], env=env) def test_uv_python_set(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("UV_PYTHON", sys.executable) project = tox_project({ "tox.ini": "[testenv]\npackage=skip\ndeps=setuptools\ncommands=python -c 'import setuptools'" }) result = project.run("-vv") result.assert_success() def test_uv_pip_constraints(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": f""" [testenv] package=skip setenv= PIP_CONSTRAINTS={os.devnull} commands=python --version """ }) result = project.run() result.assert_success() assert ( result.out.count( "Found PIP_CONSTRAINTS defined, you may want to also define UV_CONSTRAINT to match pip behavior." ) == 1 ), "Warning should be found once and only once in output." def test_uv_pip_constraints_no(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": f""" [testenv] package=skip setenv= PIP_CONSTRAINTS={os.devnull} UV_CONSTRAINT={os.devnull} commands=python --version """ }) result = project.run() result.assert_success() assert ( "Found PIP_CONSTRAINTS defined, you may want to also define UV_CONSTRAINT to match pip behavior." not in result.out ) class _TestUvVenv(UvVenv): @staticmethod def id() -> str: return "uv-venv-test" # pragma: no cover def set_base_python(self, python_info: PythonInfo) -> None: self._base_python_searched = True self._base_python = python_info def get_python_info(self, base_python: str) -> PythonInfo | None: return self._get_python([base_python]) @pytest.mark.parametrize( ("base_python", "architecture"), [("python3.11", None), ("python3.11-32", 32), ("python3.11-64", 64)] ) def test_get_python_architecture(base_python: str, architecture: int | None) -> None: uv_venv = _TestUvVenv(create_args=mock.Mock()) python_info = uv_venv.get_python_info(base_python) assert python_info is not None assert python_info.extra["architecture"] == architecture def test_env_version_spec_no_architecture() -> None: uv_venv = _TestUvVenv(create_args=mock.MagicMock()) python_info = PythonInfo( implementation="cpython", version_info=VersionInfo( major=3, minor=11, micro=9, releaselevel="", serial=0, ), version="", is_64=True, platform="win32", extra={"architecture": None}, ) uv_venv.set_base_python(python_info) with mock.patch("sys.version_info", (0, 0, 0)): # prevent picking sys.executable assert uv_venv.env_version_spec() == "cpython3.11" @pytest.mark.parametrize("architecture", [32, 64]) def test_env_version_spec_architecture_configured(architecture: int) -> None: uv_venv = _TestUvVenv(create_args=mock.MagicMock()) python_info = PythonInfo( implementation="cpython", version_info=VersionInfo( major=3, minor=11, micro=9, releaselevel="", serial=0, ), version="", is_64=architecture == 64, platform="win32", extra={"architecture": architecture}, ) uv_venv.set_base_python(python_info) uv_arch = {32: "x86", 64: "x86_64"}[architecture] assert uv_venv.env_version_spec() == f"cpython-3.11-windows-{uv_arch}-none" @pytest.mark.skipif(sys.platform != "win32", reason="architecture configuration only on Windows") def test_env_version_spec_architecture_configured_overwrite_sys_exe() -> None: # pragma: win32 cover uv_venv = _TestUvVenv(create_args=mock.MagicMock()) (major, minor) = sys.version_info[:2] python_info = PythonInfo( implementation="cpython", version_info=VersionInfo( major=major, minor=minor, micro=0, releaselevel="", serial=0, ), version="", is_64=False, platform="win32", extra={"architecture": 32}, ) uv_venv.set_base_python(python_info) assert uv_venv.env_version_spec() == f"cpython-{major}.{minor}-windows-x86-none" tox-uv-1.25.0/tests/test_version.py000066400000000000000000000005221475612564700173210ustar00rootroot00000000000000from __future__ import annotations import sys from subprocess import check_output def test_version() -> None: from tox_uv import __version__ # noqa: PLC0415 assert __version__ def test_tox_version() -> None: output = check_output([sys.executable, "-m", "tox", "--version"], text=True) assert " with uv==" in output tox-uv-1.25.0/tox.ini000066400000000000000000000033261475612564700144010ustar00rootroot00000000000000[tox] requires = tox>=4.24.1 tox-uv>=1.23 env_list = fix 3.13 3.12 3.11 3.10 3.9 type pkg_meta skip_missing_interpreters = true [testenv] description = run the unit tests with pytest under {base_python} package = wheel wheel_build_env = .pkg pass_env = DIFF_AGAINST PYTEST_* set_env = COVERAGE_FILE = {work_dir}/.coverage.{env_name} commands = python -m pytest {tty:--color=yes} {posargs: \ --cov {env_site_packages_dir}{/}tox_uv --cov {tox_root}{/}tests \ --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ --junitxml {work_dir}{/}junit.{env_name}.xml \ tests} diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {work_dir}{/}coverage.{env_name}.xml --fail-under 100 dependency_groups = test [testenv:fix] description = format the code base to adhere to our styles, and complain about what we cannot do automatically skip_install = true deps = pre-commit-uv>=4.1.4 commands = pre-commit run --all-files --show-diff-on-failure [testenv:type] description = run type check on code base commands = mypy src tests dependency_groups = type [testenv:pkg_meta] description = check that the long description is valid skip_install = true commands = uv build --sdist --wheel --out-dir {env_tmp_dir} . twine check {env_tmp_dir}{/}* check-wheel-contents --no-config {env_tmp_dir} dependency_groups = pkg-meta [testenv:dev] description = generate a DEV environment package = editable commands = uv pip tree python -c 'import sys; print(sys.executable)' dependency_groups = dev