pax_global_header00006660000000000000000000000064151546270040014516gustar00rootroot0000000000000052 comment=e0b9d0f034bf6cd5872dcb86802f4cd8f7eb293a tox-dev-tox-uv-e0b9d0f/000077500000000000000000000000001515462700400150375ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/.github/000077500000000000000000000000001515462700400163775ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/.github/CODEOWNERS000066400000000000000000000000251515462700400177670ustar00rootroot00000000000000* @gaborbernat tox-dev-tox-uv-e0b9d0f/.github/FUNDING.yaml000066400000000000000000000000261515462700400203530ustar00rootroot00000000000000tidelift: pypi/tox-uv tox-dev-tox-uv-e0b9d0f/.github/ISSUE_TEMPLATE/000077500000000000000000000000001515462700400205625ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000011411515462700400231670ustar00rootroot00000000000000--- 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-dev-tox-uv-e0b9d0f/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000011521515462700400225510ustar00rootroot00000000000000# 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-dev-tox-uv-e0b9d0f/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000013711515462700400242270ustar00rootroot00000000000000--- 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-dev-tox-uv-e0b9d0f/.github/SECURITY.md000066400000000000000000000005551515462700400201750ustar00rootroot00000000000000# 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-dev-tox-uv-e0b9d0f/.github/dependabot.yaml000066400000000000000000000001651515462700400213720ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" tox-dev-tox-uv-e0b9d0f/.github/release.yaml000066400000000000000000000001261515462700400207020ustar00rootroot00000000000000changelog: exclude: authors: - dependabot[bot] - pre-commit-ci[bot] tox-dev-tox-uv-e0b9d0f/.github/workflows/000077500000000000000000000000001515462700400204345ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/.github/workflows/check.yaml000066400000000000000000000040451515462700400224000ustar00rootroot00000000000000name: check on: workflow_dispatch: push: branches: ["main"] tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: test: runs-on: ${{ matrix.os || 'ubuntu-latest' }} name: ${{ matrix.env }}${{ matrix.suffix || ''}} strategy: fail-fast: false matrix: env: - "3.14" - "3.13" - "3.12" - "3.11" - "3.10" - type - dev - pkg_meta os: - ubuntu-24.04 suffix: - "" include: - env: "3.14" os: windows-2025 suffix: -windows - env: "3.14" os: macos-15 suffix: -macos - env: meta-3.14 - env: meta-3.13 - env: meta-3.12 - env: meta-3.11 - env: meta-3.10 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Install tox run: uv tool install --python-preference only-managed --python 3.14 tox --with . - name: Install Python if: (startsWith(matrix.env, '3.') && matrix.env != '3.14') || startsWith(matrix.env, 'meta-') run: | VERSION="${{ matrix.env }}" VERSION="${VERSION#meta-}" uv python install --python-preference only-managed "$VERSION" - name: Setup test suite run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} env: FORCE_COLOR: "1" UV_PYTHON_PREFERENCE: "only-managed" - name: Run test suite run: tox run --skip-pkg-install -e ${{ matrix.env }} env: FORCE_COLOR: "1" PYTEST_ADDOPTS: "-vv --durations=20" DIFF_AGAINST: HEAD UV_PYTHON_PREFERENCE: "only-managed" tox-dev-tox-uv-e0b9d0f/.github/workflows/release.yaml000066400000000000000000000026471515462700400227510ustar00rootroot00000000000000name: Release to PyPI on: push: tags: ["*"] permissions: contents: read env: dists-artifact-name: python-package-distributions jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build tox-uv-bare package run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Build tox-uv meta package run: uv build --python 3.14 --python-preference only-managed --wheel meta --out-dir dist - name: Store the distribution packages uses: actions/upload-artifact@v7 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@v8 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.13.0 with: attestations: true tox-dev-tox-uv-e0b9d0f/.gitignore000066400000000000000000000001741515462700400170310ustar00rootroot00000000000000/magic .idea *.egg-info .tox/ .coverage* coverage.xml .*_cache __pycache__ **.pyc /build dist src/tox_uv/version.py uv.lock tox-dev-tox-uv-e0b9d0f/.pre-commit-config.yaml000066400000000000000000000022371515462700400213240ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.37.0 hooks: - id: check-github-workflows args: ["--verbose"] - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell additional_dependencies: ["tomli>=2.4"] - repo: https://github.com/tox-dev/tox-toml-fmt rev: "v1.9.1" hooks: - id: tox-toml-fmt - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.16.2" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.15.5" hooks: - id: ruff-format alias: ruff args: ["--exit-non-zero-on-format"] - id: ruff-check alias: ruff args: ["--exit-non-zero-on-fix"] - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.8.1" hooks: - id: prettier args: ["--print-width=120", "--prose-wrap=always"] - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes tox-dev-tox-uv-e0b9d0f/LICENSE000066400000000000000000000017771515462700400160600ustar00rootroot00000000000000Permission 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-dev-tox-uv-e0b9d0f/README.md000066400000000000000000000255411515462700400163250ustar00rootroot00000000000000# 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) - [Installation options](#installation-options) - [uv discovery](#uv-discovery) - [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) - [only_groups](#only_groups) - [uv_sync_flags](#uv_sync_flags) - [uv_sync_locked](#uv_sync_locked) - [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 ``` ### Installation options `tox-uv` is distributed as two packages: - **`tox-uv`** (recommended): Meta package that includes both the plugin and a bundled `uv` binary. This ensures `uv` is always available and provides the best out-of-box experience. - **`tox-uv-bare`**: Plugin-only package without the bundled `uv` binary. Use this in containerized environments (Docker, Kubernetes) where `uv` is pre-installed in the system to avoid downloading duplicate binaries. Example Docker usage with `tox-uv-bare`: ```dockerfile FROM python:3.12 RUN pip install uv tox tox-uv-bare # uv is already in the container, no need to bundle it again ``` ### uv discovery `tox-uv` discovers the `uv` binary in the following order: 1. **`TOX_UV_PATH` environment variable**: Explicitly specify the `uv` binary location. Useful for testing custom `uv` builds or when `uv` is installed in a non-standard location. ```bash export TOX_UV_PATH=/custom/path/to/uv tox r ``` 1. **Bundled `uv`** (when using `tox-uv` meta package): Uses the `uv` binary included with the `tox-uv` package. 1. **System `uv`** (when using `tox-uv-bare` or if bundled `uv` not found): Searches for `uv` in your system `PATH`. If `uv` cannot be found, `tox-uv` will raise an error with installation instructions. ## 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` - do not install the project, - `wheel` - install the project as a non-editable wheel, - `editable` (default) - install the project in editable mode, - `uv` - with `uv-venv-runner` uses uv directly to install the project (bypassing tox's PEP-517 packaging), with `uv-venv-lock-runner` behaves like `wheel`, - `uv-editable` - with `uv-venv-runner` uses uv directly to install in editable mode (bypassing tox's PEP-517 packaging), with `uv-venv-lock-runner` behaves like `editable`. With `uv-venv-runner`, prefer `uv`/`uv-editable` when you need non-standard features of `uv`, such as `tool.uv.sources`. With `uv-venv-lock-runner`, `uv sync` already handles installation natively so all modes work through it. ### `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 **in addition to** the project and its dependencies (maps to `uv sync --group`). For example, `dependency_groups = ["test", "docs"]` installs the project, its default dependencies, and the `test` and `docs` groups. ### `only_groups` Install **only** these [PEP 735 – Dependency Groups](https://peps.python.org/pep-0735/), excluding the project and all other dependencies (maps to `uv sync --only-group`). Use this when you need a dependency group in complete isolation, such as CI tooling from a private index. For example, `only_groups = ["ci"]` installs only the `ci` group without the project or any of its dependencies. **Key difference**: `dependency_groups` adds groups to the standard install, while `only_groups` replaces the entire install with just those groups. ### `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 ``` ### `uv_sync_locked` By default tox-uv will call `uv sync` with `--locked` argument, which is incompatible with other arguments like `--prerelease` or `--upgrade ` that you might want to add to `uv_sync_flags` for some test scenarios. You can set this to `false` to avoid such conflicts. ### 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. **Note**: When using `uv_resolution` with `dependency_groups`, all dependencies from both `deps` and `dependency_groups` are combined into a single install operation. This ensures the resolution strategy applies correctly across all requirements, preventing sequential installations from resolving transitive dependencies before the strategy can apply to overlapping direct dependencies. [resolution strategy]: https://github.com/astral-sh/uv/blob/0.1.20/README.md#resolution-strategy tox-dev-tox-uv-e0b9d0f/meta/000077500000000000000000000000001515462700400157655ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/meta/__init__.py000066400000000000000000000003201515462700400200710ustar00rootroot00000000000000"""Meta package marker for tox-uv with bundled uv. This package provides no functionality itself - it's a dependency wrapper that installs both tox-uv-bare (the actual implementation) and uv (the tool). """ tox-dev-tox-uv-e0b9d0f/meta/hatch_build.py000066400000000000000000000013031515462700400206020ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from hatchling.metadata.plugin.interface import MetadataHookInterface class CustomMetadataHook(MetadataHookInterface): def update(self, metadata: dict[str, object]) -> None: # noqa: PLR6301 version = metadata["version"] dependencies: list[str] = metadata.get("dependencies", []) # type: ignore[assignment] metadata["dependencies"] = [ f"tox-uv-bare=={version}" if dep.startswith("tox-uv-bare") else dep for dep in dependencies ] metadata["readme"] = { "content-type": "text/markdown", "text": (Path(__file__).parent.parent / "README.md").read_text(), } tox-dev-tox-uv-e0b9d0f/meta/pyproject.toml000066400000000000000000000027561515462700400207130ustar00rootroot00000000000000[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.5", "hatchling>=1.28", ] [project] name = "tox-uv" description = "Integration of uv with tox (meta package with bundled uv)." keywords = [ "environments", "isolated", "testing", "virtual", ] license = "MIT" maintainers = [ { name = "BernĂĄt GĂĄbor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: System", ] dynamic = [ "readme", "version", ] dependencies = [ "tox-uv-bare", "uv<1,>=0.9.27", ] 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" [tool.hatch] version.source = "vcs" version.raw-options.root = ".." build.targets.wheel.force-include."__init__.py" = "tox_uv_meta/__init__.py" metadata.hooks.custom.path = "hatch_build.py" tox-dev-tox-uv-e0b9d0f/meta/tests/000077500000000000000000000000001515462700400171275ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/meta/tests/test_meta_build.py000066400000000000000000000074041515462700400226520ustar00rootroot00000000000000from __future__ import annotations import re import zipfile from pathlib import Path from subprocess import check_call from tempfile import TemporaryDirectory from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from collections.abc import Iterator @pytest.fixture def built_wheel() -> Iterator[Path]: with TemporaryDirectory() as tmp_dir: dist_dir = Path(tmp_dir) meta_dir = Path(__file__).parent.parent check_call(["uv", "build", "--wheel", str(meta_dir), "--out-dir", str(dist_dir)], cwd=meta_dir) wheels = list(dist_dir.glob("tox_uv-*.whl")) assert len(wheels) == 1 yield wheels[0] def test_version_injected_into_dependency(built_wheel: Path) -> None: with zipfile.ZipFile(built_wheel) as whl: metadata_files = [name for name in whl.namelist() if name.endswith("/METADATA")] assert len(metadata_files) == 1 metadata = whl.read(metadata_files[0]).decode() version_match = re.search(r"^Version: (.+)$", metadata, re.MULTILINE) assert version_match is not None version = version_match.group(1) bare_dep_match = re.search(r"^Requires-Dist: tox-uv-bare==(.+)$", metadata, re.MULTILINE) assert bare_dep_match is not None bare_version = bare_dep_match.group(1) assert version == bare_version def test_uv_dependency_present(built_wheel: Path) -> None: with zipfile.ZipFile(built_wheel) as whl: metadata_files = [name for name in whl.namelist() if name.endswith("/METADATA")] metadata = whl.read(metadata_files[0]).decode() assert re.search(r"^Requires-Dist: uv<1,>=0\.9\.27$", metadata, re.MULTILINE) is not None def test_wheel_contains_placeholder_module(built_wheel: Path) -> None: with zipfile.ZipFile(built_wheel) as whl: assert "tox_uv_meta/__init__.py" in whl.namelist() def test_wheel_does_not_contain_tox_uv_package(built_wheel: Path) -> None: with zipfile.ZipFile(built_wheel) as whl: tox_uv_files = [name for name in whl.namelist() if name.startswith("tox_uv/")] assert not tox_uv_files, f"Meta package should not contain tox_uv package, found: {tox_uv_files}" def test_build_hook_updates_metadata() -> None: from meta.hatch_build import CustomMetadataHook # noqa: PLC0415 hook = CustomMetadataHook("test", {}) metadata: dict[str, object] = { "version": "1.2.3", "dependencies": ["tox-uv-bare", "uv<1,>=0.9.27"], } hook.update(metadata) assert metadata["dependencies"] == ["tox-uv-bare==1.2.3", "uv<1,>=0.9.27"] def test_build_hook_preserves_other_dependencies() -> None: from meta.hatch_build import CustomMetadataHook # noqa: PLC0415 hook = CustomMetadataHook("test", {}) metadata: dict[str, object] = { "version": "2.0.0", "dependencies": ["other-package>=1.0", "tox-uv-bare", "another-dep"], } hook.update(metadata) assert metadata["dependencies"] == ["other-package>=1.0", "tox-uv-bare==2.0.0", "another-dep"] def test_build_hook_injects_root_readme() -> None: from meta.hatch_build import CustomMetadataHook # noqa: PLC0415 hook = CustomMetadataHook("test", {}) metadata: dict[str, object] = {"version": "1.0.0", "dependencies": []} hook.update(metadata) readme = metadata["readme"] assert isinstance(readme, dict) assert readme["content-type"] == "text/markdown" assert readme["text"] == (Path(__file__).parents[2] / "README.md").read_text() def test_wheel_contains_root_readme(built_wheel: Path) -> None: with zipfile.ZipFile(built_wheel) as whl: metadata_files = [name for name in whl.namelist() if name.endswith("/METADATA")] metadata = whl.read(metadata_files[0]).decode() root_readme = (Path(__file__).parents[2] / "README.md").read_text() assert root_readme in metadata tox-dev-tox-uv-e0b9d0f/pyproject.toml000066400000000000000000000111461515462700400177560ustar00rootroot00000000000000[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.5", "hatchling>=1.28", ] [project] name = "tox-uv-bare" description = "Integration of uv with tox (bare package, bring your own uv)." readme = "README.md" keywords = [ "environments", "isolated", "testing", "virtual", ] license = "MIT" maintainers = [ { name = "BernĂĄt GĂĄbor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: System", ] dynamic = [ "version", ] dependencies = [ "packaging>=26", "tomli>=2.4; python_version<'3.11'", "tox<5,>=4.40", "typing-extensions>=4.15; python_version<'3.10'", ] 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.1", "diff-cover>=10.2", "pytest>=9.0.2", "pytest-cov>=7", "pytest-mock>=3.15.1", ] type = [ "ty>=0.0.17", { include-group = "test" } ] lint = [ "pre-commit-uv>=4.2" ] pkg-meta = [ "check-wheel-contents>=0.6.3", "twine>=6.2", "uv>=0.9.27" ] [tool.hatch] build.hooks.vcs.version-file = "src/tox_uv/version.py" build.targets.sdist.include = [ "/src", "/tests", ] build.targets.wheel.packages = [ "src/tox_uv" ] version.source = "vcs" [tool.ruff] line-length = 120 fix = true unsafe-fixes = true 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 "RUF067", # `__init__` module should only contain docstrings and re-exports "S104", # Possible binding to all interface ] lint.per-file-ignores."meta/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.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.pytest] ini_options.norecursedirs = "tests/data/*" ini_options.verbosity_assertions = 2 [tool.coverage] run.parallel = true run.patch = [ "subprocess" ] run.plugins = [ "covdefaults", ] paths.other = [ ".", "*/tox_uv", "*\\tox_uv", ] paths.source = [ "src", ".tox/*/lib/*/site-packages", ".tox\\*\\Lib\\site-packages", "**/src", "**\\src", ] report.fail_under = 100 report.omit = [ "src/tox_uv/_venv_query.py", ] html.show_contexts = true html.skip_covered = false [tool.ty] environment.python-version = "3.14" src.exclude = [ "meta" ] tox-dev-tox-uv-e0b9d0f/src/000077500000000000000000000000001515462700400156265ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/src/tox_uv/000077500000000000000000000000001515462700400171525ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/src/tox_uv/__init__.py000066400000000000000000000002251515462700400212620ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations from .version import version as __version__ __all__ = [ "__version__", ] tox-dev-tox-uv-e0b9d0f/src/tox_uv/_installer.py000066400000000000000000000200301515462700400216530ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import logging import sys from collections import defaultdict from collections.abc import Sequence from functools import cached_property from itertools import chain from typing import TYPE_CHECKING, Any, Final if sys.version_info >= (3, 11): # pragma: no cover (py311+) import tomllib else: # pragma: no cover (py311+) import tomli as tomllib 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 ._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 self._env.uv # type: ignore[attr-defined,no-any-return] 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: opts: list[str] = [] if pip_pre: opts.extend(("--prerelease", "allow")) if uv_resolution: opts.extend(("--resolution", uv_resolution)) install_command[opts_at : opts_at + 1] = opts return cmd def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: ANN401 # can happen if the original python was upgraded to a newer version and # the symlinks become orphan. if not self._env.env_python().resolve().is_file(): msg = "existing venv is broken" raise Recreate(msg) 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 @cached_property def _sourced_pkg_names(self) -> set[str]: pyproject_file = self._env.conf._conf.src_path.parent / "pyproject.toml" # noqa: SLF001 if not pyproject_file.exists(): # pragma: no cover return set() with pyproject_file.open("rb") as file_handler: pyproject = tomllib.load(file_handler) sources = pyproject.get("tool", {}).get("uv", {}).get("sources", {}) return {key for key, val in sources.items() if isinstance(val, dict) and val.get("workspace", False)} 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)): for pkg in arg.deps: if ( isinstance(pkg, Requirement) and pkg.name in self._sourced_pkg_names and "." not in groups["uv_editable"] ): groups["uv_editable"].append(".") continue groups["req"].append(str(pkg)) 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(pkg) for pkg 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-dev-tox-uv-e0b9d0f/src/tox_uv/_package.py000066400000000000000000000021721515462700400212600ustar00rootroot00000000000000from __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-dev-tox-uv-e0b9d0f/src/tox_uv/_package_types.py000066400000000000000000000014401515462700400225010ustar00rootroot00000000000000from __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-dev-tox-uv-e0b9d0f/src/tox_uv/_run.py000066400000000000000000000045131515462700400204720ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import logging from typing import TYPE_CHECKING from packaging.requirements import Requirement from tox.tox_env.python.dependency_groups import resolve as resolve_dependency_groups 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 _LOGGER = logging.getLogger(__name__) 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 def _install_deps(self) -> None: groups: set[str] = self.conf["dependency_groups"] uv_resolution: str = self.conf["uv_resolution"] if uv_resolution and groups: try: root: Path = self.core["package_root"] except KeyError: root = self.core["tox_root"] group_reqs = list(resolve_dependency_groups(root, groups)) deps_file = self.conf["deps"] deps_reqs = [Requirement(line) for line in deps_file.lines()] combined_reqs = deps_reqs + group_reqs _LOGGER.info( "combining deps and dependency groups for uv_resolution=%s to ensure correct resolution", uv_resolution, ) self._install(combined_reqs, PythonRun.__name__, "deps") else: super()._install_deps() def _install_dependency_groups(self) -> None: groups: set[str] = self.conf["dependency_groups"] uv_resolution: str = self.conf["uv_resolution"] if uv_resolution and groups: return super()._install_dependency_groups() __all__ = [ "UvVenvRunner", ] tox-dev-tox-uv-e0b9d0f/src/tox_uv/_run_lock.py000066400000000000000000000135221515462700400215020ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING, Literal, cast 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], default=set(), desc="dependency groups to install of the target package", ) self.conf.add_config( keys=["only_groups"], of_type=set[str], default=set(), desc="install only these dependency groups (maps to uv sync --only-group)", ) 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], default=[], desc="Additional flags to pass to uv sync (for flags not configurable via environment variables)", ) self.conf.add_config( keys=["uv_sync_locked"], of_type=bool, default=True, desc="When set to 'false', it will remove `--locked` argument from 'uv sync' implicit arguments.", ) self.conf.add_config( # type: ignore[call-overload] keys=["package"], of_type=Literal["editable", "wheel", "skip", "uv", "uv-editable"], default="editable", desc="How should the package be installed", ) self.conf.add_config( keys=["package_root", "setupdir"], of_type=Path, default=cast("Path", self.core["tox_root"]), desc="indicates where the pyproject.toml and uv.lock files exist", ) add_skip_missing_interpreters_to_core(self.core, self.options) def _setup_env(self) -> None: # noqa: C901,PLR0912 super()._setup_env() install_pkg = getattr(self.options, "install_pkg", None) if not getattr(self.options, "skip_uv_sync", False): package_root: Path = self.conf["package_root"] if not package_root.is_absolute(): package_root = self.core["tox_root"] / package_root cmd = [ self.uv, "sync", ] if package_root != self.core["tox_root"]: cmd.extend(("--directory", str(package_root))) if self.conf["uv_sync_locked"]: cmd.append("--locked") if self.conf["uv_python_preference"] != "none": cmd.extend(("--python-preference", self.conf["uv_python_preference"])) if self.conf["uv_resolution"]: cmd.extend(("--resolution", self.conf["uv_resolution"])) 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 in {"wheel", "uv"}: project_file = package_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)) for group in sorted(self.conf["only_groups"]): cmd.extend(("--only-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._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-dev-tox-uv-e0b9d0f/src/tox_uv/_venv.py000066400000000000000000000367611515462700400206560ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations import contextlib import json import logging import os import shutil import subprocess # noqa: S404 import sys from abc import ABC from functools import cached_property from importlib.resources import as_file, files from pathlib import Path from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, cast 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 PY_FACTORS_RE, PY_FACTORS_RE_EXPLICIT_VERSION, Python, PythonInfo, VersionInfo from virtualenv.app_data import make_app_data from virtualenv.discovery.cached_py_info import from_exe from virtualenv.discovery.py_info import PythonInfo as VirtualenvPythonInfo from virtualenv.discovery.py_spec import PythonSpec from ._installer import UvInstaller if TYPE_CHECKING: from tox.execute.api import Execute from tox.tox_env.api import ToxEnvCreateArgs from tox.tox_env.installer import Installer PythonPreference: TypeAlias = Literal[ "none", "only-managed", "managed", "system", "only-system", ] _LOGGER: Final[logging.Logger] = logging.getLogger(__name__) class UvVenv(Python, ABC): def __init__(self, create_args: ToxEnvCreateArgs) -> 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.", ) def uv_python_preference_default(conf: object, name: object) -> str: # noqa: ARG001 return ( "none" if {"UV_NO_MANAGED_PYTHON", "UV_MANAGED_PYTHON"} & set(os.environ) else os.environ.get("UV_PYTHON_PREFERENCE", "system") ) def uv_python_preference_post_process(value: str | None) -> str: if value is not None: return value.lower() return "system" self.conf.add_config( # ty: ignore[no-matching-overload] keys=["uv_python_preference"], of_type=cast("type[PythonPreference | None]", PythonPreference | None), # 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=uv_python_preference_default, 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." ), post_process=uv_python_preference_post_process, ) 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"] env_dir = cast("Path", self.conf["env_dir"]) if not env_dir.is_absolute(): env_dir = cast("Path", self.core["tox_root"]) / env_dir result["venv"] = str(self.venv_dir.relative_to(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: for base in base_python: # pragma: no branch base_path = Path(base) if base_path.is_absolute(): # pragma: win32 no cover env_spec = PythonSpec.from_string_spec(self.name) has_python_factor = any( PY_FACTORS_RE.match(f) or PY_FACTORS_RE_EXPLICIT_VERSION.match(f) for f in self.name.split("-") ) if env_spec.major is not None and has_python_factor: spec = env_spec elif (base_from_name := self.extract_base_python(self.name)) is not None: spec = PythonSpec.from_string_spec(base_from_name) else: info = self._get_virtualenv_py_info(base_path) vi = info.version_info return PythonInfo( implementation=info.implementation, version_info=VersionInfo( major=int(vi.major), minor=int(vi.minor), micro=int(vi.micro), releaselevel=str(vi.releaselevel), serial=int(vi.serial), ), version=info.version, is_64=info.architecture == 64, # noqa: PLR2004 platform=info.platform, extra={"executable": str(base_path)}, free_threaded=bool(info.free_threaded), ) else: spec = PythonSpec.from_string_spec(base) return PythonInfo( implementation=spec.implementation or "CPython", version_info=VersionInfo( major=spec.major or 0, minor=spec.minor or 0, micro=spec.micro or 0, releaselevel="", serial=0, ), version=str(spec), is_64=spec.architecture == 64, # noqa: PLR2004 platform=sys.platform, extra={"architecture": spec.architecture}, free_threaded=bool(spec.free_threaded), ) return None # pragma: no cover @staticmethod def _get_virtualenv_py_info(path: Path) -> VirtualenvPythonInfo: # pragma: win32 no cover result = from_exe( VirtualenvPythonInfo, make_app_data(None, read_only=False, env=os.environ), str(path), ) if result is None: # pragma: no cover msg = f"failed to discover Python info for {path}" raise RuntimeError(msg) return result @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 """ info = cls._get_virtualenv_py_info(path) # pragma: win32 no cover return PythonSpec.from_string_spec( # pragma: win32 no cover f"{info.implementation}{info.version_info.major}{info.version_info.minor}-{info.architecture}", ) @staticmethod def _get_uv_version(uv_path: str) -> str: try: result = subprocess.run( # noqa: S603 [uv_path, "--version"], capture_output=True, text=True, check=False, timeout=5, ) return result.stdout.strip() if result.returncode == 0 else "unknown" except (subprocess.TimeoutExpired, OSError): return "unknown" @cached_property def uv(self) -> str: # Check for explicit override first if uv_env := os.environ.get("TOX_UV_PATH"): if not (uv_path := shutil.which(uv_env)): msg = f"TOX_UV_PATH={uv_env} not found in PATH" raise RuntimeError(msg) version = self._get_uv_version(uv_path) _LOGGER.warning("using uv from TOX_UV_PATH: %s (%s)", uv_path, version) return uv_path # Try bundled uv (when installed via tox-uv meta package) with contextlib.suppress(ImportError, FileNotFoundError): from uv import find_uv_bin # type: ignore[import-not-found] # noqa: PLC0415 uv_bin = find_uv_bin() # pragma: no cover _LOGGER.debug("using bundled uv from: %s", uv_bin) # pragma: no cover return uv_bin # pragma: no cover # Fall back to system uv (when using tox-uv-bare) if not (uv_path := shutil.which("uv")): # pragma: no cover msg = ( # pragma: no cover "uv not found. Either:\n" " 1. Install with bundled uv: pip install tox-uv\n" " 2. Install tox-uv-bare and ensure system uv is in PATH: which uv\n" " 3. Set TOX_UV_PATH environment variable to uv binary location" ) raise RuntimeError(msg) # pragma: no cover version = self._get_uv_version(uv_path) _LOGGER.debug("using system uv from PATH: %s (%s)", uv_path, version) return uv_path @property def venv_dir(self) -> Path: result = cast("Path", self.conf["env_dir"]) if not result.is_absolute(): result = cast("Path", self.core["tox_root"]) / result return result @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"] cmd.append(f"--prompt={self.core._root.name}[{self.name}]") # noqa: SLF001 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 in {1, 2}: 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: # pragma: win32 no cover if sys.platform == "win32": # pragma: win32 cover return self.venv_dir / "Lib" / "site-packages" 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: if executable := self.base_python.extra.get("executable"): return executable base = self.base_python.version_info imp = self.base_python.impl_lower architecture = self.base_python.extra.get("architecture") free_threaded = self.base_python.free_threaded if architecture is not None and self.base_python.platform == "win32": uv_arch = {32: "x86", 64: "x86_64"}[architecture] uv_imp = imp or "" free_threaded_tag = "+freethreaded" if free_threaded else "" version_spec = f"{uv_imp}-{base.major}.{base.minor}{free_threaded_tag}-windows-{uv_arch}-none" else: uv_imp = imp or "" free_threaded_tag = "+freethreaded" if free_threaded else "" if not base.major: # pragma: win32 no cover version_spec = f"{uv_imp}" elif not base.minor: version_spec = f"{uv_imp}{base.major}{free_threaded_tag}" else: version_spec = f"{uv_imp}{base.major}.{base.minor}{free_threaded_tag}" 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-dev-tox-uv-e0b9d0f/src/tox_uv/_venv_query.py000066400000000000000000000005011515462700400220620ustar00rootroot00000000000000from __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-dev-tox-uv-e0b9d0f/src/tox_uv/plugin.py000066400000000000000000000023601515462700400210230ustar00rootroot00000000000000"""GitHub Actions integration.""" from __future__ import annotations from importlib.metadata import PackageNotFoundError, 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: try: uv_version = version("uv") except PackageNotFoundError: return "" return f"with uv=={uv_version}" __all__ = [ "tox_register_tox_env", ] tox-dev-tox-uv-e0b9d0f/src/tox_uv/py.typed000066400000000000000000000000001515462700400206370ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/000077500000000000000000000000001515462700400162015ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/conftest.py000066400000000000000000000022621515462700400204020ustar00rootroot00000000000000from __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_workspace(root: Path) -> Path: return root / "demo_pkg_workspace" @pytest.fixture(scope="session") def demo_pkg_no_pyproject(root: Path) -> Path: return root / "demo_pkg_no_pyproject" @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-dev-tox-uv-e0b9d0f/tests/demo_pkg_inline/000077500000000000000000000000001515462700400213245ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_inline/build.py000066400000000000000000000103041515462700400227730ustar00rootroot00000000000000from __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-dev-tox-uv-e0b9d0f/tests/demo_pkg_inline/pyproject.toml000066400000000000000000000001571515462700400242430ustar00rootroot00000000000000[build-system] build-backend = "build" requires = [] backend-path = [ ".", ] [tool.black] line-length = 120 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/000077500000000000000000000000001515462700400225615ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/setup.cfg000066400000000000000000000001151515462700400243770ustar00rootroot00000000000000[metadata] name=demo-pkg version=0.0.1 [options] [bdist_wheel] universal=1 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/setup.py000066400000000000000000000001621515462700400242720ustar00rootroot00000000000000from __future__ import annotations from setuptools import setup setup(name="demo-pkg", package_dir={"": "src"}) tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/src/000077500000000000000000000000001515462700400233505ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/src/demo_pkg/000077500000000000000000000000001515462700400251355ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/src/demo_pkg/__init__.py000066400000000000000000000000001515462700400272340ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/000077500000000000000000000000001515462700400222675ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/demo_pkg_setuptools/000077500000000000000000000000001515462700400263555ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py000066400000000000000000000001651515462700400304700ustar00rootroot00000000000000from __future__ import annotations def do() -> None: print("greetings from demo_pkg_setuptools") # noqa: T201 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/pyproject.toml000066400000000000000000000001321515462700400251770ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools>=63", ] tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/setup.cfg000066400000000000000000000001221515462700400241030ustar00rootroot00000000000000[metadata] name = demo_pkg_setuptools version = 1.2.3 [options] packages = find: tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/000077500000000000000000000000001515462700400220445ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/README.md000066400000000000000000000000001515462700400233110ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/000077500000000000000000000000001515462700400236225ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/000077500000000000000000000000001515462700400254115ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/README.md000066400000000000000000000000001515462700400266560ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/pyproject.toml000066400000000000000000000012151515462700400303240ustar00rootroot00000000000000[build-system] build-backend = "uv_build" requires = [ "uv-build>=0.8.9,<0.9" ] [project] name = "demo-foo" version = "0.1.0" description = "Add your description here" readme = "README.md" authors = [ { name = "Sorin Sbarnea", email = "sorin.sbarnea@gmail.com" } ] requires-python = ">=3.9" classifiers = [ "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", "Programming Language :: Python :: 3.14", ] dependencies = [ "demo-root" ] tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/src/000077500000000000000000000000001515462700400262005ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/src/demo_foo/000077500000000000000000000000001515462700400277675ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/src/demo_foo/__init__.py000066400000000000000000000001331515462700400320750ustar00rootroot00000000000000from __future__ import annotations def hello() -> str: return "Hello from demo-foo!" tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/src/demo_foo/py.typed000066400000000000000000000000001515462700400314540ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/pyproject.toml000066400000000000000000000015211515462700400247570ustar00rootroot00000000000000[build-system] build-backend = "uv_build" requires = [ "uv-build>=0.8.9,<0.9" ] [project] name = "demo-root" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.9" classifiers = [ "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", "Programming Language :: Python :: 3.14", ] # typing-extensions is not really used but we include it for testing the # branch coverage for deps that are not sourced. dependencies = [ "demo-foo", "typing-extensions" ] [tool.uv] sources.demo-foo = { workspace = true } sources.demo-root = { workspace = true } workspace.members = [ "packages/*" ] tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/000077500000000000000000000000001515462700400226335ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/demo_root/000077500000000000000000000000001515462700400246225ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/demo_root/__init__.py000066400000000000000000000000001515462700400267210ustar00rootroot00000000000000tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/demo_root/main.py000066400000000000000000000001521515462700400261160ustar00rootroot00000000000000from __future__ import annotations def main() -> None: pass if __name__ == "__main__": main() tox-dev-tox-uv-e0b9d0f/tests/test_tox_uv_api.py000066400000000000000000000011601515462700400217650ustar00rootroot00000000000000from __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-dev-tox-uv-e0b9d0f/tests/test_tox_uv_installer.py000066400000000000000000000203201515462700400232100ustar00rootroot00000000000000from __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", "packaging"} 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"] def test_uv_install_with_resolution_strategy_and_pip_pre(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = tomli>=2.0.1 package = skip uv_resolution = lowest-direct pip_pre = true """ }) 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", "--prerelease", "allow", "--resolution", "lowest-direct", "tomli>=2.0.1", "-v", ] def test_uv_install_with_resolution_strategy_and_dependency_groups(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = packaging>=20.0 package = skip uv_resolution = lowest-direct dependency_groups = test """, "pyproject.toml": """ [project] name = "test-pkg" version = "0.1.0" [dependency-groups] test = ["pytest>=8.0.0"] """, }) execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = project.run("-vv") result.assert_success() install_calls = [call[0][3].cmd for call in execute_calls.call_args_list if "install" in call[0][3].run_id] assert len(install_calls) == 1 cmd = install_calls[0][2:] assert cmd[0] == "install" assert "--resolution" in cmd assert "lowest-direct" in cmd assert "packaging>=20.0" in cmd assert "pytest>=8.0.0" in cmd def test_uv_install_lowest_direct_with_dependency_groups_and_package(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = tomli>=2.0.0 uv_resolution = lowest-direct dependency_groups = test package = skip """, "pyproject.toml": """ [project] name = "test-pkg" version = "0.1.0" dependencies = ["packaging>=20.0"] [dependency-groups] test = ["pytest>=8.0.0"] """, }) execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = project.run("-vv") result.assert_success() install_calls = [call[0][3].cmd for call in execute_calls.call_args_list if "install" in call[0][3].run_id] assert len(install_calls) == 1 cmd = install_calls[0][2:] assert cmd[0] == "install" assert "--resolution" in cmd assert "lowest-direct" in cmd assert "tomli>=2.0.0" in cmd assert "pytest>=8.0.0" in cmd def test_uv_install_with_skip_env_install(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] deps = packaging>=20.0 package = skip uv_resolution = lowest-direct dependency_groups = test """, "pyproject.toml": """ [project] name = "test-pkg" version = "0.1.0" [dependency-groups] test = ["pytest>=8.0.0"] """, }) execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = project.run("-vv", "--skip-env-install") result.assert_success() install_calls = [call[0][3].cmd for call in execute_calls.call_args_list if "install" in call[0][3].run_id] assert len(install_calls) == 0 assert "skip installing dependencies and package" in result.out def test_uv_install_with_pylock(tox_project: ToxProjectCreator) -> None: pylock_content = """ version = 1 [[package]] name = "tomli" version = "2.0.1" """ project = tox_project({ "tox.ini": """ [testenv] package = skip pylock = pylock.toml """, "pyproject.toml": """ [project] name = "test-pkg" version = "0.1.0" """, "pylock.toml": pylock_content, }) result = project.run("-vv") result.assert_failed() assert "uv cannot install" in result.out def test_uv_install_broken_venv(tox_project: ToxProjectCreator) -> None: """Tests ability to detect that a venv a with broken symlink to python interpreter is recreated.""" project = tox_project({ "tox.ini": """ [testenv] skip_install = true install = false commands = {env_python} --version """ }) result = project.run("run", "-v") result.assert_success() assert "recreate env because existing venv is broken" not in result.out # break the environment if sys.platform != "win32": # pragma: win32 no cover bin_dir = project.path / ".tox" / "py" / "bin" executables = ("python", "python3") else: # pragma: win32 cover bin_dir = project.path / ".tox" / "py" / "Scripts" executables = ("python.exe", "pythonw.exe") bin_dir.mkdir(parents=True, exist_ok=True) for filename in executables: path = bin_dir / filename path.unlink(missing_ok=True) path.symlink_to("/broken-location") # run again and ensure we did run the repair bits result = project.run("run", "-v") result.assert_success() assert "recreate env because existing venv is broken" in result.out tox-dev-tox-uv-e0b9d0f/tests/test_tox_uv_lock.py000066400000000000000000000561051515462700400221550ustar00rootroot00000000000000from __future__ import annotations import shutil import sys from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from tox.pytest import ToxProjectCreator @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_lock_with_setupdir(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """\ [testenv] runner = uv-venv-lock-runner package_root = src """, }) (project.path / "src").mkdir() execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("run", "--notest", "-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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "-v", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ uv, "sync", "--directory", str(project.path / "src"), "--locked", "--python-preference", "system", "-v", "-p", sys.executable, ], ), ] assert calls == expected @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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "-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 = shutil.which("uv") v_args = ["-v"] if verbose not in {"", "-v"} else [] prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", *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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "-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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "-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") @pytest.mark.parametrize("uv_sync_locked", [True, False]) def test_uv_sync_extra_flags(tox_project: ToxProjectCreator, uv_sync_locked: bool) -> None: uv_sync_locked_str = str(uv_sync_locked).lower() project = tox_project({ "tox.ini": f""" [testenv] runner = uv-venv-lock-runner no_default_groups = false uv_sync_flags = --no-editable, --inexact uv_sync_locked = {uv_sync_locked_str} 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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ uv, "sync", *(["--locked"] if uv_sync_locked else []), "--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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", *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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected @pytest.mark.parametrize("package", ["wheel", "uv"]) def test_uv_package_non_editable(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch, package: str) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) project = tox_project({ "tox.toml": f""" [env_run_base] runner = "uv-venv-lock-runner" package = "{package}" """, "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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--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 @pytest.mark.parametrize("package", ["wheel", "uv"]) def test_uv_package_non_editable_no_pyproject( tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch, package: str ) -> None: monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False) project = tox_project({ "tox.toml": f""" [env_run_base] runner = "uv-venv-lock-runner" package = "{package}" """, }) project.patch_execute(lambda r: 0 if r.run_id != "venv" else None) result = project.run("run", "--notest") result.assert_failed() def test_uv_package_uv_editable(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 = "uv-editable" """, }) 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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ uv, "sync", "--locked", "--python-preference", "system", "-p", sys.executable, ], ), ] assert calls == expected 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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--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 @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_lock_ith_resolution(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """ [testenv] runner = uv-venv-lock-runner uv_resolution = highest """ }) 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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ uv, "sync", "--locked", "--python-preference", "system", "--resolution", "highest", "-p", sys.executable, ], ), ] assert len(calls) == len(expected) for i in range(len(calls)): assert calls[i] == expected[i] @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_sync_only_groups(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.toml": """\ [env_run_base] runner = "uv-venv-lock-runner" only_groups = ["ci"] 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 = shutil.which("uv") prompt = f"{project.path.name}[py]" expected = [ ( "py", "venv", [ uv, "venv", "-p", sys.executable, "--allow-existing", f"--prompt={prompt}", "--python-preference", "system", str(project.path / ".tox" / "py"), ], ), ( "py", "uv-sync", [ uv, "sync", "--locked", "--python-preference", "system", "--only-group", "ci", "-p", sys.executable, ], ), ("py", "commands[0]", ["python", "hello"]), ] assert calls == expected tox-dev-tox-uv-e0b9d0f/tests/test_tox_uv_package.py000066400000000000000000000056461515462700400226240ustar00rootroot00000000000000from __future__ import annotations 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 = """ [testenv] package=editable-legacy [testenv:.pkg] uv_seed = true """ 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() def test_uv_package_list_sources(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None: ini = """ [testenv] """ project = tox_project({"tox.ini": ini}, base=demo_pkg_inline) pyproject = project.path / "pyproject.toml" pyproject.write_text( pyproject.read_text() + """\ [tool.uv.sources] some-dep = [ {index = "foo", marker = "sys_platform == 'linux'"}, {index = "bar", marker = "sys_platform == 'darwin'"}, ] """ ) result = project.run() result.assert_success() def test_uv_package_workspace(tox_project: ToxProjectCreator, demo_pkg_workspace: Path) -> None: """Tests ability to install uv workspace projects.""" ini = """ [testenv] [testenv:.pkg] uv_seed = true """ project = tox_project({"tox.ini": ini}, base=demo_pkg_workspace) result = project.run() result.assert_success() def test_uv_package_no_pyproject(tox_project: ToxProjectCreator, demo_pkg_no_pyproject: Path) -> None: """Tests ability to install uv workspace projects.""" ini = """ [testenv] [testenv:.pkg] uv_seed = true """ project = tox_project({"tox.ini": ini}, base=demo_pkg_no_pyproject) result = project.run() result.assert_success() tox-dev-tox-uv-e0b9d0f/tests/test_tox_uv_venv.py000066400000000000000000000646031515462700400222050ustar00rootroot00000000000000from __future__ import annotations import importlib.util import os import os.path import pathlib import platform import shutil import subprocess import sys from configparser import ConfigParser 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 pytest_mock import MockerFixture 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() @pytest.mark.parametrize("env_name", ["doc8", "flake8", "check2"]) def test_uv_venv_non_python_env_name_with_trailing_digit(tox_project: ToxProjectCreator, env_name: str) -> None: project = tox_project({ "tox.ini": f"[testenv:{env_name}]\nskip_install = true\ncommands = python --version", }) result = project.run("-vve", env_name) result.assert_success() assert f"-p {env_name}" not in result.out 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" @pytest.mark.usefixtures("clear_python_preference_env_var") def test_uv_venv_preference_empty_falls_back_to_system(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "[testenv]\nuv_python_preference ="}) 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" @pytest.mark.usefixtures("clear_python_preference_env_var") @pytest.mark.parametrize("env_var", ["UV_NO_MANAGED_PYTHON", "UV_MANAGED_PYTHON"]) def test_uv_venv_preference_not_set_if_uv_no_managed_python( tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch, env_var: str ) -> None: # --(no-)managed-python cannot be used together with --python-preference project = tox_project({"tox.ini": "[testenv]"}) monkeypatch.setenv(env_var, "True") result = project.run("c", "-k", "uv_python_preference") result.assert_success() parser = ConfigParser() parser.read_string(result.out) got = parser["testenv:py"] assert got.get("uv_python_preference") == "none" 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.xfail( sys.platform == "win32", reason="Bug https://github.com/tox-dev/tox-uv/issues/193 https://github.com/astral-sh/uv/issues/14239", ) @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: # pragma: win32 no cover 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"), [ ("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 @pytest.mark.xfail( sys.platform == "win32", reason="Bug https://github.com/tox-dev/tox-uv/issues/193 https://github.com/astral-sh/uv/issues/14239", ) def test_uv_venv_spec_abs_path( tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path ) -> None: # pragma: win32 no cover project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={other_interpreter_exe}"}) result = project.run("-vv") result.assert_success() @pytest.mark.xfail( sys.platform == "win32", reason="Bug https://github.com/tox-dev/tox-uv/issues/193 https://github.com/astral-sh/uv/issues/14239", ) def test_uv_venv_spec_abs_path_conflict_ver( tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path ) -> None: # pragma: win32 no cover # 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 @pytest.mark.xfail( sys.platform == "win32", reason="Bug https://github.com/tox-dev/tox-uv/issues/193 https://github.com/astral-sh/uv/issues/14239", ) def test_uv_venv_spec_abs_path_conflict_impl( tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path ) -> None: # pragma: win32 no cover 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 @pytest.mark.parametrize("env_name", ["control2", "build3", "lint", "myenv"]) def test_uv_venv_non_python_env_name(tox_project: ToxProjectCreator, env_name: str) -> None: project = tox_project({ "tox.ini": f"[testenv:{env_name}]\npackage=skip\nskip_install=true\ncommands=python --version" }) result = project.run("-vve", env_name) result.assert_success() 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") result.assert_failed(code=1) def test_uv_venv_na_uv_072(tox_project: ToxProjectCreator) -> None: # Test uv==0.7.2 # skip_missing_interpreters is true by default project = tox_project({"tox.ini": "[testenv]\npackage=skip\nbase_python=1.0\nrequires=uv==0.7.2"}) result = project.run("-vv") 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=2) 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") 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() uv_path = shutil.which("uv") assert uv_path is not None uv_result = subprocess.run([uv_path, "--version"], capture_output=True, text=True, check=True) ver = uv_result.stdout.strip().split()[1] 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 @pytest.mark.parametrize( "env", ["3.10", "3.10-onlymanaged"], ) def test_uv_env_python_preference_complex( tox_project: ToxProjectCreator, *, env: str, ) -> None: project = tox_project({ "tox.ini": ( "[tox]\n" "env_list =\n" " 3.10\n" "[testenv]\n" "package=skip\n" "uv_python_preference=\n" " onlymanaged: only-managed\n" "commands=python -c 'print(\"{env_python}\")'" ) }) result = project.run("-vv", "-e", env) result.assert_success() exe = "python.exe" if sys.platform == "win32" else "python" env_bin_dir = str(project.path / ".tox" / env / ("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}" uv_path = shutil.which("uv") uv_dir = str(pathlib.Path(uv_path).parent) if uv_path else None 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() or path == uv_dir ) # 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]) def test_get_python_abs_path_with_python_env_name() -> None: # pragma: win32 no cover uv_venv = _TestUvVenv(create_args=mock.Mock()) with mock.patch.object(type(uv_venv), "name", new_callable=mock.PropertyMock, return_value="py999"): python_info = uv_venv.get_python_info(sys.executable) assert python_info is not None assert python_info.version_info.major == 9 assert python_info.version_info.minor == 99 assert python_info.implementation == "CPython" @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 @pytest.mark.parametrize(("base_python", "is_free_threaded"), [("py313", False), ("py313t", True)]) def test_get_python_free_threaded(base_python: str, is_free_threaded: 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.free_threaded == is_free_threaded @pytest.mark.parametrize("env_name", ["pypy", "cpython"]) def test_get_python_abs_path_with_impl(env_name: str) -> None: create_args = mock.Mock() create_args.conf = mock.MagicMock() create_args.conf.__getitem__.return_value = env_name uv_venv = _TestUvVenv(create_args=create_args) python_info = uv_venv.get_python_info(sys.executable) assert python_info is not None expected_impl = "CPython" if env_name == "cpython" else env_name assert python_info.implementation.lower() == expected_impl.lower() 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" def test_env_version_spec_free_threaded() -> None: uv_venv = _TestUvVenv(create_args=mock.MagicMock()) python_info = PythonInfo( implementation="cpython", version_info=VersionInfo( major=3, minor=13, micro=3, releaselevel="", serial=0, ), version="", is_64=True, platform="win32", extra={"architecture": None}, free_threaded=True, ) 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.13+freethreaded" def test_relative_workdir_with_changedir(tox_project: ToxProjectCreator) -> None: project = tox_project({ "tox.ini": """\ [tox] toxworkdir=.tox [testenv:demo] changedir={toxinidir}/sub package = skip commands = python -c "print('ok')" allowlist_externals = python """, }) (project.path / "sub").mkdir() result = project.run("-e", "demo") result.assert_success() def test_uv_path_env_var_invalid(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TOX_UV_PATH", "nonexistent_uv_binary") project = tox_project({"tox.ini": "[testenv]\npackage=skip"}) with pytest.raises(RuntimeError, match="TOX_UV_PATH=nonexistent_uv_binary not found in PATH"): project.run() def test_uv_path_env_var_valid(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: uv_path = shutil.which("uv") assert uv_path is not None monkeypatch.setenv("TOX_UV_PATH", "uv") project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python --version"}) result = project.run() result.assert_success() def test_uv_version_timeout(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: mock_run = mocker.patch("subprocess.run", side_effect=subprocess.TimeoutExpired("uv", 5)) project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python --version"}) result = project.run() result.assert_success() mock_run.assert_called() def test_uv_version_os_error(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: mock_run = mocker.patch("subprocess.run", side_effect=OSError("mock error")) project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python --version"}) result = project.run() result.assert_success() mock_run.assert_called() def test_uv_bundled_import_error(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: import builtins # noqa: PLC0415 from typing import Any # noqa: PLC0415 original_import = builtins.__import__ def mock_import(name: str, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401 if name == "uv": msg = "mocked import error" raise ImportError(msg) return original_import(name, *args, **kwargs) mocker.patch("builtins.__import__", side_effect=mock_import) project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python --version"}) result = project.run() result.assert_success() tox-dev-tox-uv-e0b9d0f/tests/test_version.py000066400000000000000000000016241515462700400213020ustar00rootroot00000000000000from __future__ import annotations import sys from subprocess import check_output from typing import TYPE_CHECKING if TYPE_CHECKING: from pytest_mock import MockerFixture 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 "tox-uv" in output def test_plugin_version_info_without_uv_package() -> None: from tox_uv.plugin import tox_append_version_info # noqa: PLC0415 result = tox_append_version_info() assert not result def test_plugin_version_info_with_uv_package(mocker: MockerFixture) -> None: from tox_uv.plugin import tox_append_version_info # noqa: PLC0415 mocker.patch("tox_uv.plugin.version", return_value="0.10.5") result = tox_append_version_info() assert result == "with uv==0.10.5" tox-dev-tox-uv-e0b9d0f/tox.toml000066400000000000000000000063741515462700400165600ustar00rootroot00000000000000requires = [ "tox>=4.34.1", "tox-uv>=1.23" ] env_list = [ "3.14", "meta-3.14", "3.13", "3.12", "3.11", "3.10", "fix", "pkg_meta", "type" ] skip_missing_interpreters = true [env_run_base] description = "run the unit tests with pytest under {env_name}" package = "wheel" wheel_build_env = ".pkg" dependency_groups = [ "test" ] pass_env = [ "DIFF_AGAINST", "PYTEST_*" ] set_env.COVERAGE_FILE = { replace = "env", name = "COVERAGE_FILE", default = "{work_dir}{/}.coverage.{env_name}" } commands = [ [ "python", "-m", "pytest", { replace = "posargs", default = [ "--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", ], extend = true }, ], [ "diff-cover", "--compare-branch", { replace = "env", name = "DIFF_AGAINST", default = "origin/main" }, "{work_dir}{/}coverage.{env_name}.xml", "--fail-under", "100", ], ] [env.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.2" ] commands = [ [ "pre-commit", "run", "--all-files", "--show-diff-on-failure" ] ] [env.pkg_meta] description = "check that the long description is valid" skip_install = true dependency_groups = [ "pkg-meta" ] commands = [ [ "uv", "build", "--sdist", "--wheel", "--out-dir", "{env_tmp_dir}", "." ], [ "twine", "check", "{env_tmp_dir}{/}*" ], [ "check-wheel-contents", "--no-config", "{env_tmp_dir}" ], ] [env.type] description = "run type check on code base" dependency_groups = [ "type" ] commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "." ] ] [env.dev] description = "generate a DEV environment" package = "editable" dependency_groups = [ "dev" ] commands = [ [ "uv", "pip", "tree" ], [ "python", "-c", "import sys; print(sys.executable)" ], ] [env.meta] description = "run meta package tests to verify build and version injection" package = "skip" deps = [ "hatchling>=1.28" ] dependency_groups = [ "test" ] commands = [ [ "python", "-m", "pytest", { replace = "posargs", default = [ "--cov", "{tox_root}{/}meta", "--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", "meta/tests", "-v", ], extend = true }, ], [ "diff-cover", "--compare-branch", { replace = "env", name = "DIFF_AGAINST", default = "origin/main" }, "{work_dir}{/}coverage.{env_name}.xml", "--fail-under", "100", ], ] [env."meta-3.10"] base = [ "meta" ] [env."meta-3.11"] base = [ "meta" ] [env."meta-3.12"] base = [ "meta" ] [env."meta-3.13"] base = [ "meta" ] [env."meta-3.14"] base = [ "meta" ]