Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dynamic = [
]
dependencies = [
"packaging>=24.2",
"tomli>=2.2.1; python_version<'3.11'",
"tox>=4.26,<5",
"typing-extensions>=4.12.2; python_version<'3.10'",
"uv>=0.5.31,<1",
Expand All @@ -66,7 +67,7 @@ test = [
"pytest-cov>=6",
"pytest-mock>=3.14",
]
type = [ "mypy==1.15", { include-group = "test" } ]
type = [ "mypy==1.15", "types-setuptools>=80.9.0.20250801", { 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" ]

Expand Down
29 changes: 27 additions & 2 deletions src/tox_uv/_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
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
Expand Down Expand Up @@ -101,6 +107,17 @@ def install(self, arguments: Any, section: str, of_type: str) -> None: # noqa:
_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 val.get("workspace", False)}

def _install_list_of_deps( # noqa: C901, PLR0912
self,
arguments: Sequence[
Expand All @@ -114,12 +131,20 @@ def _install_list_of_deps( # noqa: C901, PLR0912
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)
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(i) for i in arg.deps)
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 ""
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ 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"
Expand Down
8 changes: 8 additions & 0 deletions tests/demo_pkg_no_pyproject/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[metadata]
name=demo-pkg
version=0.0.1

[options]

[bdist_wheel]
universal=1
5 changes: 5 additions & 0 deletions tests/demo_pkg_no_pyproject/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import annotations

from setuptools import setup

setup(name="demo-pkg", package_dir={"": "src"})
Empty file.
Empty file.
Empty file.
20 changes: 20 additions & 0 deletions tests/demo_pkg_workspace/packages/demo_foo/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[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 = "[email protected]" } ]
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",
]
dependencies = [ "demo-root" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import annotations


def hello() -> str:
return "Hello from demo-foo!"
Empty file.
28 changes: 28 additions & 0 deletions tests/demo_pkg_workspace/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[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",
]
# 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 }
demo-root = { workspace = true }

[tool.uv.workspace]
members = [ "packages/*" ]
Empty file.
9 changes: 9 additions & 0 deletions tests/demo_pkg_workspace/src/demo_root/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations


def main() -> None:
pass


if __name__ == "__main__":
main()
28 changes: 28 additions & 0 deletions tests/test_tox_uv_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,31 @@ 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_workspace(tox_project: ToxProjectCreator, demo_pkg_workspace: Path) -> None:
"""Tests ability to install uv workspace projects."""
ini = f"""
[testenv]

[testenv:.pkg]
uv_seed = true
{"deps = wheel" if sys.version_info >= (3, 12) else ""}
"""
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 = f"""
[testenv]

[testenv:.pkg]
uv_seed = true
{"deps = wheel" if sys.version_info >= (3, 12) else ""}
"""
project = tox_project({"tox.ini": ini}, base=demo_pkg_no_pyproject)
result = project.run()
result.assert_success()
Loading