pax_global_header 0000666 0000000 0000000 00000000064 14756125647 0014533 g ustar 00root root 0000000 0000000 52 comment=4218b8705287628e59b51d5462bf0df61313a7a0
tox-uv-1.25.0/ 0000775 0000000 0000000 00000000000 14756125647 0013062 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/.github/ 0000775 0000000 0000000 00000000000 14756125647 0014422 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/.github/CODEOWNERS 0000664 0000000 0000000 00000000025 14756125647 0016012 0 ustar 00root root 0000000 0000000 * @gaborbernat
tox-uv-1.25.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000026 14756125647 0016235 0 ustar 00root root 0000000 0000000 tidelift: pypi/tox-uv
tox-uv-1.25.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14756125647 0016605 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/.github/ISSUE_TEMPLATE/bug-report.md 0000664 0000000 0000000 00000001141 14756125647 0021212 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-uv-1.25.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000001152 14756125647 0020574 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-uv-1.25.0/.github/ISSUE_TEMPLATE/feature-request.md 0000664 0000000 0000000 00000001371 14756125647 0022252 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-uv-1.25.0/.github/SECURITY.md 0000664 0000000 0000000 00000000555 14756125647 0016220 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-uv-1.25.0/.github/dependabot.yml 0000664 0000000 0000000 00000000165 14756125647 0017254 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
tox-uv-1.25.0/.github/release.yml 0000664 0000000 0000000 00000000114 14756125647 0016561 0 ustar 00root root 0000000 0000000 changelog:
exclude:
authors:
- dependabot
- pre-commit-ci
tox-uv-1.25.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14756125647 0016457 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/.github/workflows/check.yaml 0000664 0000000 0000000 00000002625 14756125647 0020425 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
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
env:
- "3.13"
- "3.12"
- "3.11"
- "3.10"
- "3.9"
- type
- dev
- pkg_meta
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
- name: Install tox
run: uv tool install --python-preference only-managed --python 3.13 tox --with .
- name: Install Python
if: startsWith(matrix.env, '3.') && matrix.env != '3.13'
run: uv python install --python-preference only-managed ${{ matrix.env }}
- name: Setup test suite
run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }}
env:
UV_PYTHON_PREFERENCE: "only-managed"
- name: Run test suite
run: tox run --skip-pkg-install -e ${{ matrix.env }}
env:
PYTEST_ADDOPTS: "-vv --durations=20"
DIFF_AGAINST: HEAD
UV_PYTHON_PREFERENCE: "only-managed"
tox-uv-1.25.0/.github/workflows/release.yaml 0000664 0000000 0000000 00000002363 14756125647 0020767 0 ustar 00root root 0000000 0000000 name: Release to PyPI
on:
push:
tags: ["*"]
env:
dists-artifact-name: python-package-distributions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build package
run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: ${{ env.dists-artifact-name }}
path: dist/*
release:
needs:
- build
runs-on: ubuntu-latest
environment:
name: release
url: https://pypi.org/project/tox-uv/${{ github.ref_name }}
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: ${{ env.dists-artifact-name }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
attestations: true
tox-uv-1.25.0/.gitignore 0000664 0000000 0000000 00000000164 14756125647 0015053 0 ustar 00root root 0000000 0000000 /magic
.idea
*.egg-info
.tox/
.coverage*
coverage.xml
.*_cache
__pycache__
**.pyc
/build
dist
src/tox_uv/version.py
tox-uv-1.25.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000002171 14756125647 0017344 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.1
hooks:
- id: check-github-workflows
args: ["--verbose"]
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies: ["tomli>=2.2.1"]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: "1.5.0"
hooks:
- id: tox-ini-fmt
args: ["-p", "fix"]
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.5.0"
hooks:
- id: pyproject-fmt
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.9.6"
hooks:
- id: ruff-format
- id: ruff
args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
- repo: https://github.com/rbubley/mirrors-prettier
rev: "v3.5.1"
hooks:
- id: prettier
args: ["--print-width=120", "--prose-wrap=always"]
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
tox-uv-1.25.0/LICENSE 0000664 0000000 0000000 00000001777 14756125647 0014103 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-uv-1.25.0/README.md 0000664 0000000 0000000 00000017205 14756125647 0014346 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)
- [tox environment types provided](#tox-environment-types-provided)
- [uv.lock support](#uvlock-support)
- [package](#package)
- [extras](#extras)
- [no_default_groups](#no_default_groups)
- [dependency_groups](#dependency_groups)
- [uv_sync_flags](#uv_sync_flags)
- [External package support](#external-package-support)
- [Environment creation](#environment-creation)
- [uv_seed](#uv_seed)
- [uv_python_preference](#uv_python_preference)
- [Package installation](#package-installation)
- [uv_resolution](#uv_resolution)
## How to use
Install `tox-uv` into the environment of your tox, and it will replace `virtualenv` and `pip` for all runs:
```bash
uv tool install tox --with tox-uv # use uv to install
tox --version # validate you are using the installed tox
tox r -e py312 # will use uv
tox --runner virtualenv r -e py312 # will use virtualenv+pip
```
## tox environment types provided
This package will provide the following new tox environments:
- `uv-venv-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner) for
environments not using a lock file.
- `uv-venv-lock-runner` is the ID for the tox environments [runner](https://tox.wiki/en/4.12.1/config.html#runner) for
environments using `uv.lock` (note we can’t detect the presence of the `uv.lock` file to enable this because that
would break environments not using the lock file - such as your linter).
- `uv-venv-pep-517` is the ID for the PEP-517 packaging environment.
- `uv-venv-cmd-builder` is the ID for the external cmd builder.
## uv.lock support
If you want for a tox environment to use `uv sync` with a `uv.lock` file you need to change for that tox environment the
`runner` to `uv-venv-lock-runner`. Furthermore, should in such environments you use the `extras` config to instruct `uv`
to install the specified extras, for example (this example is for the `tox.ini`, for other formats see the documentation
[here](https://tox.wiki/en/latest/config.html#discovery-and-file-types)):
```ini
[testenv:fix]
description = run code formatter and linter (auto-fix)
skip_install = true
deps =
pre-commit-uv>=4.1.1
commands =
pre-commit run --all-files --show-diff-on-failure
[testenv:type]
runner = uv-venv-lock-runner
description = run type checker via mypy
commands =
mypy {posargs:src}
[testenv:dev]
runner = uv-venv-lock-runner
description = dev environment
extras =
dev
test
type
commands =
uv pip tree
```
In this example:
- `fix` will use the `uv-venv-runner` and use `uv pip install` to install dependencies to the environment.
- `type` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment without any
extra group.
- `dev` will use the `uv-venv-lock-runner` and use `uv sync` to install dependencies to the environment with the `dev`,
`test` and `type` extra groups.
Note that when using `uv-venv-lock-runner`, _all_ dependencies will come from the lock file, controlled by `extras`.
Therefore, options like `deps` are ignored (and all others
[enumerated here](https://tox.wiki/en/stable/config.html#python-run) as Python run flags).
### `package`
How to install the source tree package, must be one of:
- `skip`,
- `wheel`,
- `editable` (default),
- `uv` (use uv to install the project, rather than build wheel via `tox`),
- `uv-editable` (use uv to install the project in editable mode, rather than build wheel via `tox`).
You should use the latter two in case you need to use any non-standard features of `uv`, such as `tool.uv.sources`.
### `extras`
A list of string that selects, which extra groups you want to install with `uv sync`. By default, it is empty.
### `no_default_groups`
A boolean flag to toggle installation of the `uv`
[default development groups](https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups). By default, it
will be `true` if the `dependency_groups` is not empty and `false` otherwise.
### `dependency_groups`
Specify [PEP 735 – Dependency Groups](https://peps.python.org/pep-0735/) to install.
### `uv_sync_flags`
A list of strings, containing additional flags to pass to uv sync (useful because some flags are not configurable via
environment variables). For example, if you want to install the package in non editable mode and keep extra packages
installed into the environment you can do:
```ini
uv_sync_flags = --no-editable, --inexact
```
### External package support
Should tox be invoked with the [`--installpkg`](https://tox.wiki/en/stable/cli_interface.html#tox-run---installpkg) flag
(the argument **must** be either a wheel or source distribution) the sync operation will run with `--no-install-project`
and `uv pip install` will be used afterward to install the provided package.
## Environment creation
We use `uv venv` to create virtual environments. This process can be configured with the following options:
### `uv_seed`
This flag, set on a tox environment level, controls if the created virtual environment injects `pip`, `setuptools` and
`wheel` into the created virtual environment or not. By default, it is off. You will need to set this if you have a
project that uses the old legacy-editable mode, or your project doesn’t support the `pyproject.toml` powered isolated
build model.
### `uv_python_preference`
This flag, set on a tox environment level, controls how `uv` select the Python interpreter.
By default, `uv` will attempt to use Python versions found on the system and only download managed interpreters when
necessary. However, It is possible to adjust `uv`'s Python version selection preference with the
[python-preference](https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences) option.
### `system_site_packages` (`sitepackages`)
Create virtual environments that also have access to globally installed packages. Note the default value may be
overwritten by the VIRTUALENV_SYSTEM_SITE_PACKAGES environment variable. This flag works the same way as the one from
[tox native virtualenv implementation](https://tox.wiki/en/latest/config.html#system_site_packages).
## Package installation
We use `uv pip` to install packages into the virtual environment. The behavior of this can be configured via the
following options:
### `uv_resolution`
This flag, set on a tox environment level, informs `uv` of the desired [resolution strategy]:
- `highest` - (default) selects the highest version of a package satisfying the constraints.
- `lowest` - install the **lowest** compatible versions for all dependencies, both **direct** and **transitive**.
- `lowest-direct` - opt for the **lowest** compatible versions for all **direct** dependencies, while using the
**latest** compatible versions for all **transitive** dependencies.
This is an `uv` specific feature that may be used as an alternative to frozen constraints for test environments if the
intention is to validate the lower bounds of your dependencies during test executions.
[resolution strategy]: https://github.com/astral-sh/uv/blob/0.1.20/README.md#resolution-strategy
tox-uv-1.25.0/pyproject.toml 0000664 0000000 0000000 00000010220 14756125647 0015771 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "hatchling.build"
requires = [
"hatch-vcs>=0.4",
"hatchling>=1.27",
]
[project]
name = "tox-uv"
description = "Integration of uv with tox."
readme = "README.md"
keywords = [
"environments",
"isolated",
"testing",
"virtual",
]
license = "MIT"
maintainers = [
{ name = "Bernát Gábor", email = "gaborjbernat@gmail.com" },
]
requires-python = ">=3.9"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Internet",
"Topic :: Software Development :: Libraries",
"Topic :: System",
]
dynamic = [
"version",
]
dependencies = [
"packaging>=24.2",
"tox>=4.24.1,<5",
"typing-extensions>=4.12.2; python_version<'3.10'",
"uv>=0.5.31,<1",
]
urls.Changelog = "https://github.com/tox-dev/tox-uv/releases"
urls.Documentation = "https://github.com/tox-dev/tox-uv#tox-uv"
urls.Homepage = "https://github.com/tox-dev/tox-uv"
urls.Source = "https://github.com/tox-dev/tox-uv"
urls.Tracker = "https://github.com/tox-dev/tox-uv/issues"
entry-points.tox.tox-uv = "tox_uv.plugin"
[dependency-groups]
dev = [
{ include-group = "lint" },
{ include-group = "pkg-meta" },
{ include-group = "test" },
{ include-group = "type" },
]
test = [
"covdefaults>=2.3",
"devpi-process>=1.0.2",
"diff-cover>=9.2.2",
"pytest>=8.3.4",
"pytest-cov>=6",
"pytest-mock>=3.14",
]
type = [ "mypy==1.15", { include-group = "test" } ]
lint = [ "pre-commit-uv>=4.1.4" ]
pkg-meta = [ "check-wheel-contents>=0.6.1", "twine>=6.1", "uv>=0.5.31" ]
[tool.hatch]
build.hooks.vcs.version-file = "src/tox_uv/version.py"
build.targets.sdist.include = [
"/src",
"/tests",
]
version.source = "vcs"
[tool.ruff]
line-length = 120
format.preview = true
format.docstring-code-line-length = 100
format.docstring-code-format = true
lint.select = [
"ALL",
]
lint.ignore = [
"COM812", # Conflict with formatter
"CPY", # No copyright statements
"D", # no documentation for now
"D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible
"D205", # 1 blank line required between summary line and description
"D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible
"D301", # Use `r"""` if any backslashes in a docstring
"D401", # First line of docstring should be in imperative mood
"DOC201", # no support for sphinx
"ISC001", # Conflict with formatter
"S104", # Possible binding to all interface
]
lint.per-file-ignores."tests/**/*.py" = [
"D", # don't care about documentation in tests
"FBT", # don't care about booleans as positional arguments in tests
"INP001", # no implicit namespace
"PLC2701", # private import is fine
"PLR2004", # Magic value used in comparison, consider replacing with a constant variable
"S", # no safety concerns
"S101", # asserts allowed in tests...
]
lint.isort = { known-first-party = [
"tox_uv",
"tests",
], required-imports = [
"from __future__ import annotations",
] }
lint.preview = true
[tool.codespell]
builtin = "clear,usage,en-GB_to_en-US"
write-changes = true
count = true
[tool.pyproject-fmt]
max_supported_python = "3.13"
[tool.pytest.ini_options]
norecursedirs = "tests/data/*"
verbosity_assertions = 2
[tool.coverage]
html.show_contexts = true
html.skip_covered = false
paths.source = [
"src",
".tox/*/lib/*/site-packages",
".tox\\*\\Lib\\site-packages",
"**/src",
"**\\src",
]
paths.other = [
".",
"*/tox_uv",
"*\\tox_uv",
]
report.omit = [
"src/tox_uv/_venv_query.py",
]
report.fail_under = 100
run.parallel = true
run.plugins = [
"covdefaults",
]
[tool.mypy]
python_version = "3.12"
show_error_codes = true
strict = true
overrides = [
{ module = [
"virtualenv.*",
"uv.*",
], ignore_missing_imports = true },
]
tox-uv-1.25.0/src/ 0000775 0000000 0000000 00000000000 14756125647 0013651 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/src/tox_uv/ 0000775 0000000 0000000 00000000000 14756125647 0015175 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/src/tox_uv/__init__.py 0000664 0000000 0000000 00000000225 14756125647 0017305 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
from .version import version as __version__
__all__ = [
"__version__",
]
tox-uv-1.25.0/src/tox_uv/_installer.py 0000664 0000000 0000000 00000015520 14756125647 0017706 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
import logging
from collections import defaultdict
from collections.abc import Sequence
from itertools import chain
from typing import TYPE_CHECKING, Any, Final
from packaging.requirements import Requirement
from packaging.utils import parse_sdist_filename, parse_wheel_filename
from tox.config.types import Command
from tox.tox_env.errors import Fail, Recreate
from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage
from tox.tox_env.python.pip.pip_install import Pip
from tox.tox_env.python.pip.req_file import PythonDeps
from uv import find_uv_bin
from ._package_types import UvEditablePackage, UvPackage
if TYPE_CHECKING:
from tox.config.main import Config
from tox.tox_env.package import PathPackage
from tox.tox_env.python.api import Python
_LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
class UvInstaller(Pip):
"""Pip is a python installer that can install packages as defined by PEP-508 and PEP-517."""
def __init__(self, tox_env: Python, with_list_deps: bool = True) -> None: # noqa: FBT001, FBT002
self._with_list_deps = with_list_deps
super().__init__(tox_env)
def freeze_cmd(self) -> list[str]:
return [self.uv, "--color", "never", "pip", "freeze"]
@property
def uv(self) -> str:
return find_uv_bin()
def _register_config(self) -> None:
super()._register_config()
def uv_resolution_post_process(value: str) -> str:
valid_opts = {"highest", "lowest", "lowest-direct"}
if value and value not in valid_opts:
msg = f"Invalid value for uv_resolution: {value!r}. Valid options are: {', '.join(valid_opts)}."
raise Fail(msg)
return value
self._env.conf.add_config(
keys=["uv_resolution"],
of_type=str,
default="",
desc="Define the resolution strategy for uv",
post_process=uv_resolution_post_process,
)
def default_install_command(self, conf: Config, env_name: str | None) -> Command: # noqa: ARG002
cmd = [self.uv, "pip", "install", "{opts}", "{packages}"]
if self._env.options.verbosity > 3: # noqa: PLR2004
cmd.append("-v")
return Command(cmd)
def post_process_install_command(self, cmd: Command) -> Command:
install_command = cmd.args
pip_pre: bool = self._env.conf["pip_pre"]
uv_resolution: str = self._env.conf["uv_resolution"]
try:
opts_at = install_command.index("{opts}")
except ValueError:
if pip_pre:
install_command.extend(("--prerelease", "allow"))
if uv_resolution:
install_command.extend(("--resolution", uv_resolution))
else:
if pip_pre:
install_command[opts_at] = "--prerelease"
install_command.insert(opts_at + 1, "allow")
if uv_resolution:
install_command[opts_at] = "--resolution"
install_command.insert(opts_at + 1, uv_resolution)
if not (pip_pre or uv_resolution):
install_command.pop(opts_at)
return cmd
def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa: ANN401
if isinstance(arguments, PythonDeps):
self._install_requirement_file(arguments, section, of_type)
elif isinstance(arguments, Sequence): # pragma: no branch
self._install_list_of_deps(arguments, section, of_type)
else: # pragma: no cover
_LOGGER.warning("uv cannot install %r", arguments) # pragma: no cover
raise SystemExit(1) # pragma: no cover
def _install_list_of_deps( # noqa: C901, PLR0912
self,
arguments: Sequence[
Requirement | WheelPackage | SdistPackage | EditableLegacyPackage | EditablePackage | PathPackage
],
section: str,
of_type: str,
) -> None:
groups: dict[str, list[str]] = defaultdict(list)
for arg in arguments:
if isinstance(arg, Requirement): # pragma: no branch
groups["req"].append(str(arg)) # pragma: no cover
elif isinstance(arg, (WheelPackage, SdistPackage, EditablePackage)):
groups["req"].extend(str(i) for i in arg.deps)
parser = parse_sdist_filename if isinstance(arg, SdistPackage) else parse_wheel_filename
name, *_ = parser(arg.path.name)
groups["pkg"].append(f"{name}@{arg.path}")
elif isinstance(arg, EditableLegacyPackage):
groups["req"].extend(str(i) for i in arg.deps)
groups["dev_pkg"].append(str(arg.path))
elif isinstance(arg, UvPackage):
extras_suffix = f"[{','.join(arg.extras)}]" if arg.extras else ""
groups["uv"].append(f"{arg.path}{extras_suffix}")
elif isinstance(arg, UvEditablePackage):
extras_suffix = f"[{','.join(arg.extras)}]" if arg.extras else ""
groups["uv_editable"].append(f"{arg.path}{extras_suffix}")
else: # pragma: no branch
_LOGGER.warning("uv install %r", arg) # pragma: no cover
raise SystemExit(1) # pragma: no cover
req_of_type = f"{of_type}_deps" if groups["pkg"] or groups["dev_pkg"] else of_type
for value in groups.values():
value.sort()
with self._env.cache.compare(groups["req"], section, req_of_type) as (eq, old):
if not eq: # pragma: no branch
miss = sorted(set(old or []) - set(groups["req"]))
if miss: # no way yet to know what to uninstall here (transitive dependencies?) # pragma: no branch
msg = f"dependencies removed: {', '.join(str(i) for i in miss)}" # pragma: no cover
raise Recreate(msg) # pragma: no branch # pragma: no cover
new_deps = sorted(set(groups["req"]) - set(old or []))
if new_deps: # pragma: no branch
self._execute_installer(new_deps, req_of_type)
install_args = ["--reinstall"]
if groups["uv"]:
self._execute_installer(install_args + groups["uv"], of_type)
if groups["uv_editable"]:
requirements = list(chain.from_iterable(("-e", entry) for entry in groups["uv_editable"]))
self._execute_installer(install_args + requirements, of_type)
install_args.append("--no-deps")
if groups["pkg"]:
self._execute_installer(install_args + groups["pkg"], of_type)
if groups["dev_pkg"]:
for entry in groups["dev_pkg"]:
install_args.extend(("-e", str(entry)))
self._execute_installer(install_args, of_type)
__all__ = [
"UvInstaller",
]
tox-uv-1.25.0/src/tox_uv/_package.py 0000664 0000000 0000000 00000002172 14756125647 0017303 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-uv-1.25.0/src/tox_uv/_package_types.py 0000664 0000000 0000000 00000001440 14756125647 0020524 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-uv-1.25.0/src/tox_uv/_run.py 0000664 0000000 0000000 00000002036 14756125647 0016513 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
from typing import TYPE_CHECKING
from tox.tox_env.python.runner import PythonRun
from ._package_types import UvEditablePackage, UvPackage
from ._venv import UvVenv
if TYPE_CHECKING:
from pathlib import Path
class UvVenvRunner(UvVenv, PythonRun):
@staticmethod
def id() -> str:
return "uv-venv-runner"
@property
def _package_tox_env_type(self) -> str:
return "uv-venv-pep-517"
@property
def _external_pkg_tox_env_type(self) -> str:
return "uv-venv-cmd-builder" # pragma: no cover
@property
def default_pkg_type(self) -> str:
tox_root: Path = self.core["tox_root"]
if not (any((tox_root / i).exists() for i in ("pyproject.toml", "setup.py", "setup.cfg"))):
return "skip"
return super().default_pkg_type
@property
def _package_types(self) -> tuple[str, ...]:
return *super()._package_types, UvPackage.KEY, UvEditablePackage.KEY
__all__ = [
"UvVenvRunner",
]
tox-uv-1.25.0/src/tox_uv/_run_lock.py 0000664 0000000 0000000 00000011346 14756125647 0017527 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
import sys
from pathlib import Path
from typing import TYPE_CHECKING, List, Literal, Set, cast # noqa: UP035
from tox.execute.request import StdinSource
from tox.report import HandledError
from tox.tox_env.python.package import SdistPackage, WheelPackage
from tox.tox_env.python.runner import add_extras_to_env, add_skip_missing_interpreters_to_core
from tox.tox_env.runner import RunToxEnv
from ._venv import UvVenv
if sys.version_info >= (3, 11): # pragma: no cover (py311+)
import tomllib
else: # pragma: no cover (py311+)
import tomli as tomllib
if TYPE_CHECKING:
from tox.tox_env.package import Package
class UvVenvLockRunner(UvVenv, RunToxEnv):
@staticmethod
def id() -> str:
return "uv-venv-lock-runner"
def _register_package_conf(self) -> bool: # noqa: PLR6301
return False
@property
def _package_tox_env_type(self) -> str:
raise NotImplementedError
@property
def _external_pkg_tox_env_type(self) -> str:
raise NotImplementedError
def _build_packages(self) -> list[Package]:
raise NotImplementedError
def register_config(self) -> None:
super().register_config()
add_extras_to_env(self.conf)
self.conf.add_config(
keys=["dependency_groups"],
of_type=Set[str], # noqa: UP006
default=set(),
desc="dependency groups to install of the target package",
)
self.conf.add_config(
keys=["no_default_groups"],
of_type=bool,
default=lambda _, __: bool(self.conf["dependency_groups"]),
desc="Install default groups or not",
)
self.conf.add_config(
keys=["uv_sync_flags"],
of_type=List[str], # noqa: UP006
default=[],
desc="Additional flags to pass to uv sync (for flags not configurable via environment variables)",
)
self.conf.add_config(
keys=["package"],
of_type=Literal["editable", "wheel", "skip"], # type: ignore[arg-type]
default="editable",
desc="How should the package be installed",
)
add_skip_missing_interpreters_to_core(self.core, self.options)
def _setup_env(self) -> None: # noqa: C901
super()._setup_env()
install_pkg = getattr(self.options, "install_pkg", None)
if not getattr(self.options, "skip_uv_sync", False):
cmd = [
"uv",
"sync",
"--locked",
]
if self.conf["uv_python_preference"] != "none":
cmd.extend(("--python-preference", self.conf["uv_python_preference"]))
for extra in cast("set[str]", sorted(self.conf["extras"])):
cmd.extend(("--extra", extra))
groups = sorted(self.conf["dependency_groups"])
if self.conf["no_default_groups"]:
cmd.append("--no-default-groups")
package = self.conf["package"]
if install_pkg is not None or package == "skip":
cmd.append("--no-install-project")
if self.options.verbosity > 3: # noqa: PLR2004
cmd.append("-v")
if package == "wheel":
# need the package name here but we don't have the packaging infrastructure -> read from pyproject.toml
project_file = self.core["tox_root"] / "pyproject.toml"
name = None
if project_file.exists():
with project_file.open("rb") as file_handler:
raw = tomllib.load(file_handler)
name = raw.get("project", {}).get("name")
if name is None:
msg = "Could not detect project name"
raise HandledError(msg)
cmd.extend(("--no-editable", "--reinstall-package", name))
for group in groups:
cmd.extend(("--group", group))
cmd.extend(self.conf["uv_sync_flags"])
cmd.extend(("-p", self.env_version_spec()))
show = self.options.verbosity > 2 # noqa: PLR2004
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="uv-sync", show=show)
outcome.assert_success()
if install_pkg is not None:
path = Path(install_pkg)
pkg = (WheelPackage if path.suffix == ".whl" else SdistPackage)(path, deps=[])
self.installer.install([pkg], "install-pkg", of_type="external")
@property
def environment_variables(self) -> dict[str, str]:
env = super().environment_variables
env["UV_PROJECT_ENVIRONMENT"] = str(self.venv_dir)
return env
__all__ = [
"UvVenvLockRunner",
]
tox-uv-1.25.0/src/tox_uv/_venv.py 0000664 0000000 0000000 00000027633 14756125647 0016677 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
import json
import logging
import os
import sys
from abc import ABC
from functools import cached_property
from importlib.resources import as_file, files
from pathlib import Path
from platform import python_implementation
from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Type, cast # noqa: UP035
from tox.config.loader.str_convert import StrConvert
from tox.execute.local_sub_process import LocalSubProcessExecutor
from tox.execute.request import StdinSource
from tox.tox_env.errors import Skip
from tox.tox_env.python.api import Python, PythonInfo, VersionInfo
from tox.tox_env.python.virtual_env.api import VirtualEnv
from uv import find_uv_bin
from virtualenv.discovery.py_spec import PythonSpec
from ._installer import UvInstaller
if sys.version_info >= (3, 10): # pragma: no cover (py310+)
from typing import TypeAlias
else: # pragma: no cover ( None:
self._executor: Execute | None = None
self._installer: UvInstaller | None = None
self._created = False
self._displayed_uv_constraint_warning = False
super().__init__(create_args)
def register_config(self) -> None:
super().register_config()
self.conf.add_config(
keys=["uv_seed"],
of_type=bool,
default=False,
desc="add seed packages to the created venv",
)
self.conf.add_config(
keys=["system_site_packages", "sitepackages"],
of_type=bool,
default=lambda conf, name: StrConvert().to_bool( # noqa: ARG005
self.environment_variables.get("VIRTUALENV_SYSTEM_SITE_PACKAGES", "False"),
),
desc="create virtual environments that also have access to globally installed packages.",
)
# The cast(...) might seems superfluous but removing it makes mypy crash. The problem isy on tox typing side.
self.conf.add_config(
keys=["uv_python_preference"],
of_type=cast("Type[Optional[PythonPreference]]", Optional[PythonPreference]), # type: ignore[valid-type] # noqa: UP006
# use os.environ here instead of self.environment_variables as this value is needed to create the virtual
# environment, if environment variables use env_site_packages_dir we would run into a chicken-egg problem.
default=lambda conf, name: os.environ.get("UV_PYTHON_PREFERENCE", "system"), # noqa: ARG005
desc=(
"Whether to prefer using Python installations that are already"
" present on the system, or those that are downloaded and"
" installed by uv [possible values: none, only-managed, installed,"
" managed, system, only-system]. Use none to use uv's"
" default. Our default value is 'system', while uv's default"
" value is 'managed' because we prefer using same python"
" interpreters with all tox environments and avoid accidental"
" downloading of other interpreters."
),
)
def python_cache(self) -> dict[str, Any]:
result = super().python_cache()
result["seed"] = self.conf["uv_seed"]
if self.conf["uv_python_preference"] != "none":
result["python_preference"] = self.conf["uv_python_preference"]
result["venv"] = str(self.venv_dir.relative_to(self.env_dir))
return result
@property
def executor(self) -> Execute:
if self._executor is None:
self._executor = LocalSubProcessExecutor(self.options.is_colored)
return self._executor
@property
def installer(self) -> Installer[Any]:
if self._installer is None:
self._installer = UvInstaller(self)
return self._installer
@property
def runs_on_platform(self) -> str:
return sys.platform
def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR6301
for base in base_python: # pragma: no branch
if base == sys.executable:
version_info = sys.version_info
return PythonInfo(
implementation=python_implementation(),
version_info=VersionInfo(
major=version_info.major,
minor=version_info.minor,
micro=version_info.micro,
releaselevel=version_info.releaselevel,
serial=version_info.serial,
),
version=sys.version,
is_64=sys.maxsize > 2**32,
platform=sys.platform,
extra={},
)
base_path = Path(base)
if base_path.is_absolute():
info = VirtualEnv.get_virtualenv_py_info(base_path)
return PythonInfo(
implementation=info.implementation,
version_info=VersionInfo(*info.version_info),
version=info.version,
is_64=info.architecture == 64, # noqa: PLR2004
platform=info.platform,
extra={"executable": base},
)
spec = PythonSpec.from_string_spec(base)
return PythonInfo(
implementation=spec.implementation or "CPython",
version_info=VersionInfo(
major=spec.major,
minor=spec.minor,
micro=spec.micro,
releaselevel="",
serial=0,
),
version=str(spec),
is_64=spec.architecture == 64, # noqa: PLR2004
platform=sys.platform,
extra={"architecture": spec.architecture},
)
return None # pragma: no cover
@classmethod
def python_spec_for_path(cls, path: Path) -> PythonSpec:
"""
Get the spec for an absolute path to a Python executable.
:param path: the path investigated
:return: the found spec
"""
return VirtualEnv.python_spec_for_path(path)
@property
def uv(self) -> str:
return find_uv_bin()
@property
def venv_dir(self) -> Path:
return cast("Path", self.conf["env_dir"])
@property
def environment_variables(self) -> dict[str, str]:
env = super().environment_variables
env.pop("UV_PYTHON", None) # UV_PYTHON takes precedence over VIRTUAL_ENV
env["VIRTUAL_ENV"] = str(self.venv_dir)
if "UV_CONSTRAINT" not in env and not self._displayed_uv_constraint_warning:
for pip_var in ("PIP_CONSTRAINT", "PIP_CONSTRAINTS"):
if pip_var in env:
_LOGGER.warning(
"Found %s defined, you may want to also define UV_CONSTRAINT to match pip behavior.", pip_var
)
self._displayed_uv_constraint_warning = True
break
return env
def _default_pass_env(self) -> list[str]:
env = super()._default_pass_env()
env.append("UV_*") # accept uv env vars
if sys.platform == "darwin": # pragma: darwin cover
env.append("MACOSX_DEPLOYMENT_TARGET") # needed for macOS binary builds
env.append("PKG_CONFIG_PATH") # needed for binary builds
return env
def create_python_env(self) -> None:
version_spec = self.env_version_spec()
cmd: list[str] = [self.uv, "venv", "-p", version_spec, "--allow-existing"]
if self.options.verbosity > 3: # noqa: PLR2004
cmd.append("-v")
if self.conf["uv_seed"]:
cmd.append("--seed")
if self.conf["system_site_packages"]:
cmd.append("--system-site-packages")
if self.conf["uv_python_preference"] != "none":
cmd.extend(["--python-preference", self.conf["uv_python_preference"]])
cmd.append(str(self.venv_dir))
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="venv", show=None)
if self.core["skip_missing_interpreters"] and outcome.exit_code == 1:
msg = f"could not find python interpreter with spec(s): {version_spec}"
raise Skip(msg)
outcome.assert_success()
self._created = True
@property
def _allow_externals(self) -> list[str]:
result = super()._allow_externals
result.append(self.uv)
return result
def prepend_env_var_path(self) -> list[Path]:
return [self.env_bin_dir(), Path(self.uv).parent]
def env_bin_dir(self) -> Path:
if sys.platform == "win32": # pragma: win32 cover
return self.venv_dir / "Scripts"
else: # pragma: win32 no cover # noqa: RET505
return self.venv_dir / "bin"
def env_python(self) -> Path:
suffix = ".exe" if sys.platform == "win32" else ""
return self.env_bin_dir() / f"python{suffix}"
def env_site_package_dir(self) -> Path:
if sys.platform == "win32": # pragma: win32 cover
return self.venv_dir / "Lib" / "site-packages"
# pragma: win32 no cover
py = self._py_info
impl = "pypy" if py.implementation == "pypy" else "python"
return self.venv_dir / "lib" / f"{impl}{py.version_dot}" / "site-packages"
def env_version_spec(self) -> str:
base = self.base_python.version_info
imp = self.base_python.impl_lower
executable = self.base_python.extra.get("executable")
architecture = self.base_python.extra.get("architecture")
if executable:
version_spec = str(executable)
elif (
architecture is None
and (base.major, base.minor) == sys.version_info[:2]
and (sys.implementation.name.lower() == imp)
):
version_spec = sys.executable
else:
uv_imp = imp or ""
if not base.major:
version_spec = f"{uv_imp}"
elif not base.minor:
version_spec = f"{uv_imp}{base.major}"
elif architecture is not None and self.base_python.platform == "win32":
uv_arch = {32: "x86", 64: "x86_64"}[architecture]
version_spec = f"{uv_imp}-{base.major}.{base.minor}-windows-{uv_arch}-none"
else:
version_spec = f"{uv_imp}{base.major}.{base.minor}"
return version_spec
@cached_property
def _py_info(self) -> PythonInfo: # pragma: win32 no cover
if not self._created and not self.env_python().exists(): # called during config, no environment setup
self.create_python_env()
if not self._paths:
self._paths = self.prepend_env_var_path()
with as_file(files("tox_uv") / "_venv_query.py") as filename:
cmd = [str(self.env_python()), str(filename)]
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="venv-query", show=False)
outcome.assert_success()
res = json.loads(outcome.out)
return PythonInfo(
implementation=res["implementation"],
version_info=VersionInfo(
major=res["version_info"][0],
minor=res["version_info"][1],
micro=res["version_info"][2],
releaselevel=res["version_info"][3],
serial=res["version_info"][4],
),
version=res["version"],
is_64=res["is_64"],
platform=sys.platform,
extra={},
)
__all__ = [
"UvVenv",
]
tox-uv-1.25.0/src/tox_uv/_venv_query.py 0000664 0000000 0000000 00000000501 14756125647 0020105 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-uv-1.25.0/src/tox_uv/plugin.py 0000664 0000000 0000000 00000002176 14756125647 0017053 0 ustar 00root root 0000000 0000000 """GitHub Actions integration."""
from __future__ import annotations
from importlib.metadata import version
from typing import TYPE_CHECKING
from tox.plugin import impl
from ._package import UvVenvCmdBuilder, UvVenvPep517Packager
from ._run import UvVenvRunner
from ._run_lock import UvVenvLockRunner
if TYPE_CHECKING:
from tox.config.cli.parser import ToxParser
from tox.tox_env.register import ToxEnvRegister
@impl
def tox_register_tox_env(register: ToxEnvRegister) -> None:
register.add_run_env(UvVenvRunner)
register.add_run_env(UvVenvLockRunner)
register.add_package_env(UvVenvPep517Packager)
register.add_package_env(UvVenvCmdBuilder)
register._default_run_env = UvVenvRunner.id() # noqa: SLF001
@impl
def tox_add_option(parser: ToxParser) -> None:
for key in ("run", "exec"):
parser.handlers[key][0].add_argument(
"--skip-uv-sync",
dest="skip_uv_sync",
help="skip uv sync (lock mode only)",
action="store_true",
)
def tox_append_version_info() -> str:
return f"with uv=={version('uv')}"
__all__ = [
"tox_register_tox_env",
]
tox-uv-1.25.0/src/tox_uv/py.typed 0000664 0000000 0000000 00000000000 14756125647 0016662 0 ustar 00root root 0000000 0000000 tox-uv-1.25.0/tests/ 0000775 0000000 0000000 00000000000 14756125647 0014224 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/tests/conftest.py 0000664 0000000 0000000 00000001700 14756125647 0016421 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_inline(root: Path) -> Path:
return root / "demo_pkg_inline"
@pytest.fixture
def clear_python_preference_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False)
pytest_plugins = [
"tox.pytest",
]
tox-uv-1.25.0/tests/demo_pkg_inline/ 0000775 0000000 0000000 00000000000 14756125647 0017347 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/tests/demo_pkg_inline/build.py 0000664 0000000 0000000 00000010304 14756125647 0021016 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-uv-1.25.0/tests/demo_pkg_inline/pyproject.toml 0000664 0000000 0000000 00000000160 14756125647 0022260 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "build"
requires = [
]
backend-path = [
".",
]
[tool.black]
line-length = 120
tox-uv-1.25.0/tests/demo_pkg_setuptools/ 0000775 0000000 0000000 00000000000 14756125647 0020312 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/tests/demo_pkg_setuptools/demo_pkg_setuptools/ 0000775 0000000 0000000 00000000000 14756125647 0024400 5 ustar 00root root 0000000 0000000 tox-uv-1.25.0/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py 0000664 0000000 0000000 00000000165 14756125647 0026513 0 ustar 00root root 0000000 0000000 from __future__ import annotations
def do() -> None:
print("greetings from demo_pkg_setuptools") # noqa: T201
tox-uv-1.25.0/tests/demo_pkg_setuptools/pyproject.toml 0000664 0000000 0000000 00000000132 14756125647 0023222 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = 'setuptools.build_meta'
requires = [
"setuptools>=63",
]
tox-uv-1.25.0/tests/demo_pkg_setuptools/setup.cfg 0000664 0000000 0000000 00000000122 14756125647 0022126 0 ustar 00root root 0000000 0000000 [metadata]
name = demo_pkg_setuptools
version = 1.2.3
[options]
packages = find:
tox-uv-1.25.0/tests/test_tox_uv_api.py 0000664 0000000 0000000 00000001160 14756125647 0020010 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-uv-1.25.0/tests/test_tox_uv_installer.py 0000664 0000000 0000000 00000006603 14756125647 0021243 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"}
def test_uv_install_with_pre(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\ndeps = tomli\npip_pre = true\npackage=skip"})
result = project.run("-vv")
result.assert_success()
def test_uv_install_with_pre_custom_install_cmd(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": """
[testenv]
deps = tomli
pip_pre = true
package = skip
install_command = uv pip install {packages}
"""
})
result = project.run("-vv")
result.assert_success()
def test_uv_install_without_pre_custom_install_cmd(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": """
[testenv]
deps = tomli
package = skip
install_command = uv pip install {packages}
"""
})
result = project.run("-vv")
result.assert_success()
@pytest.mark.parametrize("strategy", ["highest", "lowest", "lowest-direct"])
def test_uv_install_with_resolution_strategy(tox_project: ToxProjectCreator, strategy: str) -> None:
project = tox_project({"tox.ini": f"[testenv]\ndeps = tomli>=2.0.1\npackage = skip\nuv_resolution = {strategy}"})
execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None)
result = project.run("-vv")
result.assert_success()
assert execute_calls.call_args[0][3].cmd[2:] == ["install", "--resolution", strategy, "tomli>=2.0.1", "-v"]
def test_uv_install_with_invalid_resolution_strategy(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\ndeps = tomli>=2.0.1\npackage = skip\nuv_resolution = invalid"})
result = project.run("-vv")
result.assert_failed(code=1)
assert "Invalid value for uv_resolution: 'invalid'." in result.out
def test_uv_install_with_resolution_strategy_custom_install_cmd(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": """
[testenv]
deps = tomli>=2.0.1
package = skip
uv_resolution = lowest-direct
install_command = uv pip install {packages}
"""
})
execute_calls = project.patch_execute(lambda r: 0 if "install" in r.run_id else None)
result = project.run("-vv")
result.assert_success()
assert execute_calls.call_args[0][3].cmd[2:] == ["install", "tomli>=2.0.1", "--resolution", "lowest-direct"]
tox-uv-1.25.0/tests/test_tox_uv_lock.py 0000664 0000000 0000000 00000040404 14756125647 0020173 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from typing import TYPE_CHECKING
import pytest
from uv import find_uv_bin
if TYPE_CHECKING:
from tox.pytest import ToxProjectCreator
@pytest.mark.usefixtures("clear_python_preference_env_var")
def test_uv_lock_list_dependencies_command(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": """
[testenv]
runner = uv-venv-lock-runner
extras =
type
dev
commands = python hello
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run("--list-dependencies", "-vv")
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"-v",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--extra",
"dev",
"--extra",
"type",
"-v",
"-p",
sys.executable,
],
),
("py", "freeze", [uv, "--color", "never", "pip", "freeze"]),
("py", "commands[0]", ["python", "hello"]),
]
assert len(calls) == len(expected)
for i in range(len(calls)):
assert calls[i] == expected[i]
@pytest.mark.usefixtures("clear_python_preference_env_var")
@pytest.mark.parametrize("verbose", ["", "-v", "-vv", "-vvv"])
def test_uv_lock_command(tox_project: ToxProjectCreator, verbose: str) -> None:
project = tox_project({
"tox.ini": """
[testenv]
runner = uv-venv-lock-runner
extras =
type
dev
commands = python hello
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run(*[verbose] if verbose else [])
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
v_args = ["-v"] if verbose not in {"", "-v"} else []
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
*v_args,
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--extra",
"dev",
"--extra",
"type",
*v_args,
"-p",
sys.executable,
],
),
("py", "commands[0]", ["python", "hello"]),
]
assert calls == expected
show_uv_output = execute_calls.call_args_list[1].args[4]
assert show_uv_output is (bool(verbose))
@pytest.mark.usefixtures("clear_python_preference_env_var")
def test_uv_lock_with_default_groups(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": """
[testenv]
runner = uv-venv-lock-runner
no_default_groups = False
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run("-vv")
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"-v",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
("py", "uv-sync", ["uv", "sync", "--locked", "--python-preference", "system", "-v", "-p", sys.executable]),
]
assert calls == expected
@pytest.mark.usefixtures("clear_python_preference_env_var")
@pytest.mark.parametrize(
"name",
[
"tox_uv-1.12.2-py3-none-any.whl",
"tox_uv-1.12.2.tar.gz",
],
)
def test_uv_lock_with_install_pkg(tox_project: ToxProjectCreator, name: str) -> None:
project = tox_project({
"tox.ini": """
[testenv]
runner = uv-venv-lock-runner
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
wheel = project.path / name
wheel.write_text("")
result = project.run("-vv", "run", "--installpkg", str(wheel))
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"-v",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--no-install-project",
"-v",
"-p",
sys.executable,
],
),
(
"py",
"install_external",
[uv, "pip", "install", "--reinstall", "--no-deps", f"tox-uv@{wheel}", "-v"],
),
]
assert calls == expected
@pytest.mark.usefixtures("clear_python_preference_env_var")
def test_uv_sync_extra_flags(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": """
[testenv]
runner = uv-venv-lock-runner
no_default_groups = false
uv_sync_flags = --no-editable, --inexact
commands = python hello
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run()
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--no-editable",
"--inexact",
"-p",
sys.executable,
],
),
("py", "commands[0]", ["python", "hello"]),
]
assert calls == expected
@pytest.mark.usefixtures("clear_python_preference_env_var")
def test_uv_sync_extra_flags_toml(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.toml": """
[env_run_base]
runner = "uv-venv-lock-runner"
no_default_groups = false
uv_sync_flags = ["--no-editable", "--inexact"]
commands = [["python", "hello"]]
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run()
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--no-editable",
"--inexact",
"-p",
sys.executable,
],
),
("py", "commands[0]", ["python", "hello"]),
]
assert calls == expected
@pytest.mark.usefixtures("clear_python_preference_env_var")
def test_uv_sync_dependency_groups(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.toml": """
[env_run_base]
runner = "uv-venv-lock-runner"
with_dev = true
dependency_groups = ["test", "type"]
commands = [["python", "hello"]]
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run()
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--no-default-groups",
"--group",
"test",
"--group",
"type",
"-p",
sys.executable,
],
),
("py", "commands[0]", ["python", "hello"]),
]
assert calls == expected
@pytest.mark.parametrize(
("uv_python_preference", "injected"),
[
pytest.param("none", [], id="on"),
pytest.param("system", ["--python-preference", "system"], id="off"),
],
)
def test_uv_sync_uv_python_preference(
tox_project: ToxProjectCreator, uv_python_preference: str, injected: list[str]
) -> None:
project = tox_project({
"tox.toml": f"""
[env_run_base]
runner = "uv-venv-lock-runner"
with_dev = true
dependency_groups = ["test", "type"]
commands = [["python", "hello"]]
uv_python_preference = "{uv_python_preference}"
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run()
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
*injected,
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
*injected,
"--no-default-groups",
"--group",
"test",
"--group",
"type",
"-p",
sys.executable,
],
),
("py", "commands[0]", ["python", "hello"]),
]
assert calls == expected
def test_skip_uv_sync(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False)
project = tox_project({
"tox.toml": """
[env_run_base]
runner = "uv-venv-lock-runner"
commands = [["python", "hello"]]
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run("run", "--skip-uv-sync")
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
("py", "commands[0]", ["python", "hello"]),
]
assert calls == expected
def test_uv_package_wheel(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False)
project = tox_project({
"tox.toml": """
[env_run_base]
runner = "uv-venv-lock-runner"
package = "wheel"
""",
"pyproject.toml": """
[project]
name = "demo"
""",
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run("run", "--notest")
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--no-editable",
"--reinstall-package",
"demo",
"-p",
sys.executable,
],
),
]
assert calls == expected
def test_uv_package_wheel_no_pyproject(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False)
project = tox_project({
"tox.toml": """
[env_run_base]
runner = "uv-venv-lock-runner"
package = "wheel"
""",
})
project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run("run", "--notest")
result.assert_failed()
def test_skip_uv_package_skip(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("UV_PYTHON_PREFERENCE", raising=False)
project = tox_project({
"tox.toml": """
[env_run_base]
runner = "uv-venv-lock-runner"
package = "skip"
"""
})
execute_calls = project.patch_execute(lambda r: 0 if r.run_id != "venv" else None)
result = project.run("run", "--notest")
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd) for i in execute_calls.call_args_list]
uv = find_uv_bin()
expected = [
(
"py",
"venv",
[
uv,
"venv",
"-p",
sys.executable,
"--allow-existing",
"--python-preference",
"system",
str(project.path / ".tox" / "py"),
],
),
(
"py",
"uv-sync",
[
"uv",
"sync",
"--locked",
"--python-preference",
"system",
"--no-install-project",
"-p",
sys.executable,
],
),
]
assert calls == expected
tox-uv-1.25.0/tests/test_tox_uv_package.py 0000664 0000000 0000000 00000003414 14756125647 0020636 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from typing import TYPE_CHECKING
import pytest
if TYPE_CHECKING:
from pathlib import Path
from tox.pytest import ToxProjectCreator
def test_uv_package_skip(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip"})
result = project.run("-vv")
result.assert_success()
def test_uv_package_use_default_from_file(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip", "pyproject.toml": ""})
result = project.run("-vv")
result.assert_success()
@pytest.mark.parametrize("with_dash", [True, False], ids=["name_dash", "name_underscore"])
@pytest.mark.parametrize("package", ["sdist", "wheel", "editable", "uv", "uv-editable"])
def test_uv_package_artifact(
tox_project: ToxProjectCreator, package: str, demo_pkg_inline: Path, with_dash: bool
) -> None:
ini = f"[testenv]\npackage={package}"
if with_dash:
ini += "\n[testenv:.pkg]\nset_env = WITH_DASH = 1"
project = tox_project({"tox.ini": ini}, base=demo_pkg_inline)
result = project.run()
result.assert_success()
def test_uv_package_editable_legacy(tox_project: ToxProjectCreator, demo_pkg_setuptools: Path) -> None:
ini = f"""
[testenv]
package=editable-legacy
[testenv:.pkg]
uv_seed = true
{"deps = wheel" if sys.version_info >= (3, 12) else ""}
"""
project = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
result = project.run()
result.assert_success()
def test_uv_package_requirements(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ndeps=-r demo.txt", "demo.txt": "tomli"})
result = project.run("-vv")
result.assert_success()
tox-uv-1.25.0/tests/test_tox_uv_venv.py 0000664 0000000 0000000 00000044105 14756125647 0020223 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import importlib.util
import os
import os.path
import pathlib
import platform
import subprocess
import sys
from configparser import ConfigParser
from importlib.metadata import version
from typing import TYPE_CHECKING, get_args
from unittest import mock
import pytest
import tox.tox_env.errors
from tox.tox_env.python.api import PythonInfo, VersionInfo
from tox_uv._venv import PythonPreference, UvVenv
if TYPE_CHECKING:
from tox.pytest import ToxProjectCreator
def test_uv_venv_self(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip"})
result = project.run("-vv")
result.assert_success()
def test_uv_venv_pass_env(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip"})
result = project.run("c", "-k", "pass_env")
result.assert_success()
parser = ConfigParser()
parser.read_string(result.out)
pass_through = set(parser["testenv:py"]["pass_env"].splitlines())
if sys.platform == "darwin": # pragma: darwin cover
assert "MACOSX_DEPLOYMENT_TARGET" in pass_through
assert "UV_*" in pass_through
assert "PKG_CONFIG_PATH" in pass_through
@pytest.mark.usefixtures("clear_python_preference_env_var")
def test_uv_venv_preference_system_by_default(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]"})
result = project.run("c", "-k", "uv_python_preference")
result.assert_success()
parser = ConfigParser()
parser.read_string(result.out)
got = parser["testenv:py"]["uv_python_preference"]
assert got == "system"
def test_uv_venv_preference_override_via_env_var(
tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch
) -> None:
project = tox_project({"tox.ini": "[testenv]"})
monkeypatch.setenv("UV_PYTHON_PREFERENCE", "only-managed")
result = project.run("c", "-k", "uv_python_preference")
result.assert_success()
parser = ConfigParser()
parser.read_string(result.out)
got = parser["testenv:py"]["uv_python_preference"]
assert got == "only-managed"
def test_uv_venv_preference_override_via_env_var_and_set_env_depends_on_py(
tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch
) -> None:
project = tox_project({"tox.ini": "[testenv]\nset_env=A={env_site_packages_dir}"})
monkeypatch.setenv("UV_PYTHON_PREFERENCE", "only-managed")
result = project.run("c", "-k", "set_env")
result.assert_success()
assert str(project.path) in result.out
def test_uv_venv_spec(tox_project: ToxProjectCreator) -> None:
ver = sys.version_info
project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={ver.major}.{ver.minor}"})
result = project.run("-vv")
result.assert_success()
def test_uv_venv_spec_major_only(tox_project: ToxProjectCreator) -> None:
ver = sys.version_info
project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={ver.major}"})
result = project.run("-vv")
result.assert_success()
@pytest.mark.parametrize(
("pypy", "expected_uv_pypy"),
[
("pypy", "pypy"),
("pypy9", "pypy9"),
("pypy999", "pypy9.99"),
("pypy9.99", "pypy9.99"),
],
)
def test_uv_venv_spec_pypy(
capfd: pytest.CaptureFixture[str],
tox_project: ToxProjectCreator,
pypy: str,
expected_uv_pypy: str,
) -> None:
"""Validate that major and minor versions are correctly applied to implementations.
This test prevents a regression that occurred when the testenv name was "pypy":
the uv runner was asked to use "pypyNone" as the Python version.
The test is dependent on what PyPy interpreters are installed on the system;
if any PyPy is available then the "pypy" value will not raise a Skip exception,
and STDOUT will be captured in `result.out`.
However, it is expected that no system will have PyPy v9.x installed,
so STDOUT must be read from `capfd` after the Skip exception is caught.
Since it is unknown whether any PyPy interpreter will be installed,
the `else` block's branch coverage is disabled.
"""
project = tox_project({"tox.ini": f"[tox]\nenv_list = {pypy}"})
try:
result = project.run("config", "-vv")
except tox.tox_env.errors.Skip:
stdout, _ = capfd.readouterr()
else: # pragma: no cover (PyPy might not be available on the system)
stdout = result.out
assert "pypyNone" not in stdout
assert f"-p {expected_uv_pypy} " in stdout
@pytest.mark.parametrize(
("implementation", "expected_implementation", "expected_name"),
[
("", "cpython", "cpython"),
("py", "cpython", "cpython"),
("pypy", "pypy", "pypy"),
],
)
def test_uv_venv_spec_full_implementation(
tox_project: ToxProjectCreator,
implementation: str,
expected_implementation: str,
expected_name: str,
) -> None:
"""Validate that Python implementations are explicitly passed to uv's `-p` argument.
This test ensures that uv searches for the target Python implementation and version,
even if another implementation -- with the same language version --
is found on the path first.
This prevents a regression to a bug that occurred when PyPy 3.10 was on the PATH
and tox was invoked with `tox -e py3.10`:
uv was invoked with `-p 3.10` and found PyPy 3.10, not CPython 3.10.
"""
project = tox_project({})
result = project.run("run", "-vve", f"{implementation}9.99")
# Verify that uv was invoked with the full Python implementation and version.
assert f" -p {expected_implementation}9.99 " in result.out
# Verify that uv interpreted the `-p` argument as a Python spec, not an executable.
# This confirms that tox-uv is passing recognizable, usable `-p` arguments to uv.
assert f"no interpreter found for {expected_name} 9.99" in result.err.lower()
def test_uv_venv_system_site_packages(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\nsystem_site_packages=true"})
result = project.run("-vv")
result.assert_success()
@pytest.fixture
def other_interpreter_exe() -> pathlib.Path: # pragma: no cover
"""Returns an interpreter executable path that is not the exact same as `sys.executable`.
Necessary because `sys.executable` gets short-circuited when used as `base_python`."""
exe = pathlib.Path(sys.executable)
base_python: pathlib.Path | None = None
if exe.name in {"python", "python3"}:
# python -> pythonX.Y
ver = sys.version_info
base_python = exe.with_name(f"python{ver.major}.{ver.minor}")
elif exe.name[-1].isdigit():
# python X.Y -> python
base_python = exe.with_name(exe.stem[:-1])
elif exe.suffix == ".exe":
# python.exe <-> pythonw.exe
base_python = (
exe.with_name(exe.stem[:-1] + ".exe") if exe.stem.endswith("w") else exe.with_name(exe.stem + "w.exe")
)
if not base_python or not base_python.is_file():
pytest.fail("Tried to pick a base_python that is not sys.executable, but failed.")
return base_python
def test_uv_venv_spec_abs_path(tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path) -> None:
project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv")
result.assert_success()
def test_uv_venv_spec_abs_path_conflict_ver(
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
) -> None:
# py27 is long gone, but still matches the testenv capture regex, so we know it will fail
project = tox_project({"tox.ini": f"[testenv:py27]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv", "-e", "py27")
result.assert_failed()
assert f"failed with env name py27 conflicting with base python {other_interpreter_exe}" in result.out
def test_uv_venv_spec_abs_path_conflict_impl(
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
) -> None:
env = "pypy" if platform.python_implementation() == "CPython" else "cpython"
project = tox_project({"tox.ini": f"[testenv:{env}]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv", "-e", env)
result.assert_failed()
assert f"failed with env name {env} conflicting with base python {other_interpreter_exe}" in result.out
def test_uv_venv_na(tox_project: ToxProjectCreator) -> None:
# skip_missing_interpreters is true by default
project = tox_project({"tox.ini": "[testenv]\npackage=skip\nbase_python=1.0"})
result = project.run("-vv")
# When a Python interpreter is missing in a pytest environment, project.run
# return code is equal to -1
result.assert_failed(code=-1)
def test_uv_venv_skip_missing_interpreters_fail(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": "[tox]\nskip_missing_interpreters=false\n[testenv]\npackage=skip\nbase_python=1.0"
})
result = project.run("-vv")
result.assert_failed(code=1)
def test_uv_venv_skip_missing_interpreters_pass(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": "[tox]\nskip_missing_interpreters=true\n[testenv]\npackage=skip\nbase_python=1.0"
})
result = project.run("-vv")
# When a Python interpreter is missing in a pytest environment, project.run
# return code is equal to -1
result.assert_failed(code=-1)
def test_uv_venv_platform_check(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": f"[testenv]\nplatform={sys.platform}\npackage=skip"})
result = project.run("-vv")
result.assert_success()
def test_uv_env_bin_dir(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{env_bin_dir}\")'"})
result = project.run("-vv")
result.assert_success()
env_bin_dir = str(project.path / ".tox" / "py" / ("Scripts" if sys.platform == "win32" else "bin"))
assert env_bin_dir in result.out
def test_uv_env_has_access_to_plugin_uv(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=uv --version"})
result = project.run()
result.assert_success()
ver = version("uv")
assert f"uv {ver}" in result.out
def test_uv_env_python(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{env_python}\")'"})
result = project.run("-vv")
result.assert_success()
exe = "python.exe" if sys.platform == "win32" else "python"
env_bin_dir = str(project.path / ".tox" / "py" / ("Scripts" if sys.platform == "win32" else "bin") / exe)
assert env_bin_dir in result.out
@pytest.mark.parametrize(
"preference",
get_args(PythonPreference),
)
def test_uv_env_python_preference(
tox_project: ToxProjectCreator,
*,
preference: str,
) -> None:
project = tox_project({
"tox.ini": (
"[testenv]\n"
"package=skip\n"
f"uv_python_preference={preference}\n"
"commands=python -c 'print(\"{env_python}\")'"
)
})
result = project.run("-vv")
result.assert_success()
exe = "python.exe" if sys.platform == "win32" else "python"
env_bin_dir = str(project.path / ".tox" / "py" / ("Scripts" if sys.platform == "win32" else "bin") / exe)
assert env_bin_dir in result.out
def test_uv_env_site_package_dir_run(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{envsitepackagesdir}\")'"})
result = project.run("-vv")
result.assert_success()
env_dir = project.path / ".tox" / "py"
ver = sys.version_info
if sys.platform == "win32": # pragma: win32 cover
path = str(env_dir / "Lib" / "site-packages")
else: # pragma: win32 no cover
impl = "pypy" if sys.implementation.name.lower() == "pypy" else "python"
path = str(env_dir / "lib" / f"{impl}{ver.major}.{ver.minor}" / "site-packages")
assert path in result.out
def test_uv_env_site_package_dir_conf(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands={envsitepackagesdir}"})
result = project.run("c", "-e", "py", "-k", "commands")
result.assert_success()
env_dir = project.path / ".tox" / "py"
ver = sys.version_info
if sys.platform == "win32": # pragma: win32 cover
path = str(env_dir / "Lib" / "site-packages")
else: # pragma: win32 no cover
impl = "pypy" if sys.implementation.name.lower() == "pypy" else "python"
path = str(env_dir / "lib" / f"{impl}{ver.major}.{ver.minor}" / "site-packages")
assert path in result.out
def test_uv_env_python_not_in_path(tox_project: ToxProjectCreator) -> None:
# Make sure there is no pythonX.Y in the search path
ver = sys.version_info
exe_ext = ".exe" if sys.platform == "win32" else ""
python_exe = f"python{ver.major}.{ver.minor}{exe_ext}"
env = dict(os.environ)
env["PATH"] = os.path.pathsep.join(
path for path in env["PATH"].split(os.path.pathsep) if not (pathlib.Path(path) / python_exe).is_file()
)
# Make sure the Python interpreter can find our Tox module
tox_spec = importlib.util.find_spec("tox")
assert tox_spec is not None
tox_lines = subprocess.check_output(
[sys.executable, "-c", "import tox; print(tox.__file__);"], encoding="UTF-8", env=env
).splitlines()
assert tox_lines == [tox_spec.origin]
# Now use that Python interpreter to run Tox
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{env_python}\")'"})
tox_ini = project.path / "tox.ini"
assert tox_ini.is_file()
subprocess.check_call([sys.executable, "-m", "tox", "-c", tox_ini], env=env)
def test_uv_python_set(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("UV_PYTHON", sys.executable)
project = tox_project({
"tox.ini": "[testenv]\npackage=skip\ndeps=setuptools\ncommands=python -c 'import setuptools'"
})
result = project.run("-vv")
result.assert_success()
def test_uv_pip_constraints(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": f"""
[testenv]
package=skip
setenv=
PIP_CONSTRAINTS={os.devnull}
commands=python --version
"""
})
result = project.run()
result.assert_success()
assert (
result.out.count(
"Found PIP_CONSTRAINTS defined, you may want to also define UV_CONSTRAINT to match pip behavior."
)
== 1
), "Warning should be found once and only once in output."
def test_uv_pip_constraints_no(tox_project: ToxProjectCreator) -> None:
project = tox_project({
"tox.ini": f"""
[testenv]
package=skip
setenv=
PIP_CONSTRAINTS={os.devnull}
UV_CONSTRAINT={os.devnull}
commands=python --version
"""
})
result = project.run()
result.assert_success()
assert (
"Found PIP_CONSTRAINTS defined, you may want to also define UV_CONSTRAINT to match pip behavior."
not in result.out
)
class _TestUvVenv(UvVenv):
@staticmethod
def id() -> str:
return "uv-venv-test" # pragma: no cover
def set_base_python(self, python_info: PythonInfo) -> None:
self._base_python_searched = True
self._base_python = python_info
def get_python_info(self, base_python: str) -> PythonInfo | None:
return self._get_python([base_python])
@pytest.mark.parametrize(
("base_python", "architecture"), [("python3.11", None), ("python3.11-32", 32), ("python3.11-64", 64)]
)
def test_get_python_architecture(base_python: str, architecture: int | None) -> None:
uv_venv = _TestUvVenv(create_args=mock.Mock())
python_info = uv_venv.get_python_info(base_python)
assert python_info is not None
assert python_info.extra["architecture"] == architecture
def test_env_version_spec_no_architecture() -> None:
uv_venv = _TestUvVenv(create_args=mock.MagicMock())
python_info = PythonInfo(
implementation="cpython",
version_info=VersionInfo(
major=3,
minor=11,
micro=9,
releaselevel="",
serial=0,
),
version="",
is_64=True,
platform="win32",
extra={"architecture": None},
)
uv_venv.set_base_python(python_info)
with mock.patch("sys.version_info", (0, 0, 0)): # prevent picking sys.executable
assert uv_venv.env_version_spec() == "cpython3.11"
@pytest.mark.parametrize("architecture", [32, 64])
def test_env_version_spec_architecture_configured(architecture: int) -> None:
uv_venv = _TestUvVenv(create_args=mock.MagicMock())
python_info = PythonInfo(
implementation="cpython",
version_info=VersionInfo(
major=3,
minor=11,
micro=9,
releaselevel="",
serial=0,
),
version="",
is_64=architecture == 64,
platform="win32",
extra={"architecture": architecture},
)
uv_venv.set_base_python(python_info)
uv_arch = {32: "x86", 64: "x86_64"}[architecture]
assert uv_venv.env_version_spec() == f"cpython-3.11-windows-{uv_arch}-none"
@pytest.mark.skipif(sys.platform != "win32", reason="architecture configuration only on Windows")
def test_env_version_spec_architecture_configured_overwrite_sys_exe() -> None: # pragma: win32 cover
uv_venv = _TestUvVenv(create_args=mock.MagicMock())
(major, minor) = sys.version_info[:2]
python_info = PythonInfo(
implementation="cpython",
version_info=VersionInfo(
major=major,
minor=minor,
micro=0,
releaselevel="",
serial=0,
),
version="",
is_64=False,
platform="win32",
extra={"architecture": 32},
)
uv_venv.set_base_python(python_info)
assert uv_venv.env_version_spec() == f"cpython-{major}.{minor}-windows-x86-none"
tox-uv-1.25.0/tests/test_version.py 0000664 0000000 0000000 00000000522 14756125647 0017321 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from subprocess import check_output
def test_version() -> None:
from tox_uv import __version__ # noqa: PLC0415
assert __version__
def test_tox_version() -> None:
output = check_output([sys.executable, "-m", "tox", "--version"], text=True)
assert " with uv==" in output
tox-uv-1.25.0/tox.ini 0000664 0000000 0000000 00000003326 14756125647 0014401 0 ustar 00root root 0000000 0000000 [tox]
requires =
tox>=4.24.1
tox-uv>=1.23
env_list =
fix
3.13
3.12
3.11
3.10
3.9
type
pkg_meta
skip_missing_interpreters = true
[testenv]
description = run the unit tests with pytest under {base_python}
package = wheel
wheel_build_env = .pkg
pass_env =
DIFF_AGAINST
PYTEST_*
set_env =
COVERAGE_FILE = {work_dir}/.coverage.{env_name}
commands =
python -m pytest {tty:--color=yes} {posargs: \
--cov {env_site_packages_dir}{/}tox_uv --cov {tox_root}{/}tests \
--cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \
--cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \
--junitxml {work_dir}{/}junit.{env_name}.xml \
tests}
diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {work_dir}{/}coverage.{env_name}.xml --fail-under 100
dependency_groups = test
[testenv:fix]
description = format the code base to adhere to our styles, and complain about what we cannot do automatically
skip_install = true
deps =
pre-commit-uv>=4.1.4
commands =
pre-commit run --all-files --show-diff-on-failure
[testenv:type]
description = run type check on code base
commands =
mypy src tests
dependency_groups = type
[testenv:pkg_meta]
description = check that the long description is valid
skip_install = true
commands =
uv build --sdist --wheel --out-dir {env_tmp_dir} .
twine check {env_tmp_dir}{/}*
check-wheel-contents --no-config {env_tmp_dir}
dependency_groups = pkg-meta
[testenv:dev]
description = generate a DEV environment
package = editable
commands =
uv pip tree
python -c 'import sys; print(sys.executable)'
dependency_groups = dev