pax_global_header 0000666 0000000 0000000 00000000064 15154627004 0014516 g ustar 00root root 0000000 0000000 52 comment=e0b9d0f034bf6cd5872dcb86802f4cd8f7eb293a
tox-dev-tox-uv-e0b9d0f/ 0000775 0000000 0000000 00000000000 15154627004 0015037 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/.github/ 0000775 0000000 0000000 00000000000 15154627004 0016377 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/.github/CODEOWNERS 0000664 0000000 0000000 00000000025 15154627004 0017767 0 ustar 00root root 0000000 0000000 * @gaborbernat
tox-dev-tox-uv-e0b9d0f/.github/FUNDING.yaml 0000664 0000000 0000000 00000000026 15154627004 0020353 0 ustar 00root root 0000000 0000000 tidelift: pypi/tox-uv
tox-dev-tox-uv-e0b9d0f/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15154627004 0020562 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/.github/ISSUE_TEMPLATE/bug-report.md 0000664 0000000 0000000 00000001141 15154627004 0023167 0 ustar 00root root 0000000 0000000 ---
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.yml 0000664 0000000 0000000 00000001152 15154627004 0022551 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000001371 15154627004 0024227 0 ustar 00root root 0000000 0000000 ---
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.md 0000664 0000000 0000000 00000000555 15154627004 0020175 0 ustar 00root root 0000000 0000000 # 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.yaml 0000664 0000000 0000000 00000000165 15154627004 0021372 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
tox-dev-tox-uv-e0b9d0f/.github/release.yaml 0000664 0000000 0000000 00000000126 15154627004 0020702 0 ustar 00root root 0000000 0000000 changelog:
exclude:
authors:
- dependabot[bot]
- pre-commit-ci[bot]
tox-dev-tox-uv-e0b9d0f/.github/workflows/ 0000775 0000000 0000000 00000000000 15154627004 0020434 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/.github/workflows/check.yaml 0000664 0000000 0000000 00000004045 15154627004 0022400 0 ustar 00root root 0000000 0000000 name: 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.yaml 0000664 0000000 0000000 00000002647 15154627004 0022751 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000174 15154627004 0017031 0 ustar 00root root 0000000 0000000 /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.yaml 0000664 0000000 0000000 00000002237 15154627004 0021324 0 ustar 00root root 0000000 0000000 repos:
- 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/LICENSE 0000664 0000000 0000000 00000001777 15154627004 0016060 0 ustar 00root root 0000000 0000000 Permission 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.md 0000664 0000000 0000000 00000025541 15154627004 0016325 0 ustar 00root root 0000000 0000000 # tox-uv
[](https://badge.fury.io/py/tox-uv)
[](https://pypi.python.org/pypi/tox-uv/)
[](https://github.com/tox-dev/tox-uv/actions/workflows/check.yaml)
[](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/ 0000775 0000000 0000000 00000000000 15154627004 0015765 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/meta/__init__.py 0000664 0000000 0000000 00000000320 15154627004 0020071 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000001303 15154627004 0020602 0 ustar 00root root 0000000 0000000 from __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.toml 0000664 0000000 0000000 00000002756 15154627004 0020713 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 15154627004 0017127 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/meta/tests/test_meta_build.py 0000664 0000000 0000000 00000007404 15154627004 0022652 0 ustar 00root root 0000000 0000000 from __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.toml 0000664 0000000 0000000 00000011146 15154627004 0017756 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 15154627004 0015626 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/src/tox_uv/ 0000775 0000000 0000000 00000000000 15154627004 0017152 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/src/tox_uv/__init__.py 0000664 0000000 0000000 00000000225 15154627004 0021262 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
from .version import version as __version__
__all__ = [
"__version__",
]
tox-dev-tox-uv-e0b9d0f/src/tox_uv/_installer.py 0000664 0000000 0000000 00000020030 15154627004 0021653 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000002172 15154627004 0021260 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000001440 15154627004 0022501 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000004513 15154627004 0020472 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000013522 15154627004 0021502 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000036761 15154627004 0020656 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000000501 15154627004 0022062 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000002360 15154627004 0021023 0 ustar 00root root 0000000 0000000 """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.typed 0000664 0000000 0000000 00000000000 15154627004 0020637 0 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/ 0000775 0000000 0000000 00000000000 15154627004 0016201 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/conftest.py 0000664 0000000 0000000 00000002262 15154627004 0020402 0 ustar 00root root 0000000 0000000 from __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/ 0000775 0000000 0000000 00000000000 15154627004 0021324 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_inline/build.py 0000664 0000000 0000000 00000010304 15154627004 0022773 0 ustar 00root root 0000000 0000000 from __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.toml 0000664 0000000 0000000 00000000157 15154627004 0024243 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "build"
requires = []
backend-path = [
".",
]
[tool.black]
line-length = 120
tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/ 0000775 0000000 0000000 00000000000 15154627004 0022561 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/setup.cfg 0000664 0000000 0000000 00000000115 15154627004 0024377 0 ustar 00root root 0000000 0000000 [metadata]
name=demo-pkg
version=0.0.1
[options]
[bdist_wheel]
universal=1
tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/setup.py 0000664 0000000 0000000 00000000162 15154627004 0024272 0 ustar 00root root 0000000 0000000 from __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/ 0000775 0000000 0000000 00000000000 15154627004 0023350 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/src/demo_pkg/ 0000775 0000000 0000000 00000000000 15154627004 0025135 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_no_pyproject/src/demo_pkg/__init__.py 0000664 0000000 0000000 00000000000 15154627004 0027234 0 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/ 0000775 0000000 0000000 00000000000 15154627004 0022267 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/demo_pkg_setuptools/ 0000775 0000000 0000000 00000000000 15154627004 0026355 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py 0000664 0000000 0000000 00000000165 15154627004 0030470 0 ustar 00root root 0000000 0000000 from __future__ import annotations
def do() -> None:
print("greetings from demo_pkg_setuptools") # noqa: T201
tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/pyproject.toml 0000664 0000000 0000000 00000000132 15154627004 0025177 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=63",
]
tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_setuptools/setup.cfg 0000664 0000000 0000000 00000000122 15154627004 0024103 0 ustar 00root root 0000000 0000000 [metadata]
name = demo_pkg_setuptools
version = 1.2.3
[options]
packages = find:
tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/ 0000775 0000000 0000000 00000000000 15154627004 0022044 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/README.md 0000664 0000000 0000000 00000000000 15154627004 0023311 0 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/ 0000775 0000000 0000000 00000000000 15154627004 0023622 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/ 0000775 0000000 0000000 00000000000 15154627004 0025411 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/README.md 0000664 0000000 0000000 00000000000 15154627004 0026656 0 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/pyproject.toml 0000664 0000000 0000000 00000001215 15154627004 0030324 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 15154627004 0026200 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/src/demo_foo/ 0000775 0000000 0000000 00000000000 15154627004 0027767 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/packages/demo_foo/src/demo_foo/__init__.py 0000664 0000000 0000000 00000000133 15154627004 0032075 0 ustar 00root root 0000000 0000000 from __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.typed 0000664 0000000 0000000 00000000000 15154627004 0031454 0 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/pyproject.toml 0000664 0000000 0000000 00000001521 15154627004 0024757 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 15154627004 0022633 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/demo_root/ 0000775 0000000 0000000 00000000000 15154627004 0024622 5 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/demo_root/__init__.py 0000664 0000000 0000000 00000000000 15154627004 0026721 0 ustar 00root root 0000000 0000000 tox-dev-tox-uv-e0b9d0f/tests/demo_pkg_workspace/src/demo_root/main.py 0000664 0000000 0000000 00000000152 15154627004 0026116 0 ustar 00root root 0000000 0000000 from __future__ import annotations
def main() -> None:
pass
if __name__ == "__main__":
main()
tox-dev-tox-uv-e0b9d0f/tests/test_tox_uv_api.py 0000664 0000000 0000000 00000001160 15154627004 0021765 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000020320 15154627004 0023210 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000056105 15154627004 0022155 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000005646 15154627004 0022624 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000064603 15154627004 0022205 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000001624 15154627004 0021302 0 ustar 00root root 0000000 0000000 from __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.toml 0000664 0000000 0000000 00000006374 15154627004 0016560 0 ustar 00root root 0000000 0000000 requires = [ "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" ]