pydepinject 0.0.1.dev1__tar.gz → 0.0.2.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. pydepinject-0.0.2.dev0/MANIFEST.in +4 -0
  2. {pydepinject-0.0.1.dev1/src/pydepinject.egg-info → pydepinject-0.0.2.dev0}/PKG-INFO +4 -1
  3. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/pyproject.toml +9 -1
  4. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/pydepinject/__init__.py +68 -36
  5. pydepinject-0.0.2.dev0/src/pydepinject/backends.py +167 -0
  6. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0/src/pydepinject.egg-info}/PKG-INFO +4 -1
  7. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/pydepinject.egg-info/SOURCES.txt +2 -0
  8. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/pydepinject.egg-info/requires.txt +3 -0
  9. pydepinject-0.0.2.dev0/tests/conftest.py +19 -0
  10. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/tests/test_pydepinject.py +61 -20
  11. pydepinject-0.0.1.dev1/MANIFEST.in +0 -3
  12. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/LICENSE +0 -0
  13. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/Readme.md +0 -0
  14. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/setup.cfg +0 -0
  15. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/pydepinject.egg-info/dependency_links.txt +0 -0
  16. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/pydepinject.egg-info/top_level.txt +0 -0
  17. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/requirementmanager.egg-info/PKG-INFO +0 -0
  18. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/requirementmanager.egg-info/SOURCES.txt +0 -0
  19. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/requirementmanager.egg-info/dependency_links.txt +0 -0
  20. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/requirementmanager.egg-info/requires.txt +0 -0
  21. {pydepinject-0.0.1.dev1 → pydepinject-0.0.2.dev0}/src/requirementmanager.egg-info/top_level.txt +0 -0
@@ -0,0 +1,4 @@
1
+ graft src
2
+ recursive-include tests *.py
3
+ exclude Dockerfile noxfile.py tasks.py scripts/*
4
+ global-exclude *~ *.py[cod] *.so .devcontainer/* .vscode/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pydepinject
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.2.dev0
4
4
  Summary: A package to dynamically inject requirements into a virtual environment.
5
5
  Author: pydepinject
6
6
  License: MIT
@@ -21,6 +21,8 @@ Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
+ Requires-Dist: packaging>=22.0
25
+ Requires-Dist: typing_extensions
24
26
  Provides-Extra: lint
25
27
  Requires-Dist: ruff==0.4.7; extra == "lint"
26
28
  Requires-Dist: pyright==1.1.365; extra == "lint"
@@ -28,6 +30,7 @@ Requires-Dist: isort==5.13.2; extra == "lint"
28
30
  Provides-Extra: test
29
31
  Requires-Dist: pytest==8.2.1; extra == "test"
30
32
  Requires-Dist: pytest-cov==5.0.0; extra == "test"
33
+ Requires-Dist: pytest-xdist==3.6.1; extra == "test"
31
34
  Provides-Extra: build
32
35
  Requires-Dist: check-manifest==0.49; extra == "build"
33
36
  Requires-Dist: build==1.2.1; extra == "build"
@@ -15,6 +15,10 @@ authors = [
15
15
  license = { text="MIT" }
16
16
  keywords = ["virtualenv", "requirements", "dependency management"]
17
17
  requires-python = ">=3.9"
18
+ dependencies = [
19
+ "packaging>=22.0",
20
+ "typing_extensions",
21
+ ]
18
22
  classifiers = [
19
23
  "Development Status :: 4 - Beta",
20
24
  "Intended Audience :: Developers",
@@ -42,6 +46,7 @@ lint = [
42
46
  test = [
43
47
  "pytest==8.2.1",
44
48
  "pytest-cov==5.0.0",
49
+ "pytest-xdist==3.6.1",
45
50
  ]
46
51
  build = [
47
52
  "check-manifest==0.49",
@@ -62,7 +67,9 @@ log_level = "DEBUG"
62
67
 
63
68
  [tool.pytest.ini_options]
64
69
  pythonpath = "src"
65
-
70
+ testpaths = ["tests"]
71
+ addopts = "--durations=10 -n auto"
72
+ # Estimated test times: 74s with auto (2), 76s with 6 workers, 113se with no workers.
66
73
 
67
74
  [tool.ruff]
68
75
  preview = true
@@ -130,3 +137,4 @@ twine = {version = "5.1.0"}
130
137
 
131
138
  [tool.custom.ci]
132
139
  python_versions = ["3.9", "3.10", "3.11", "3.12"]
140
+ packaging_versions = ["22", "23", "24"]
@@ -6,50 +6,71 @@ import logging
6
6
  import os
7
7
  import pathlib
8
8
  import shutil
9
- import subprocess
10
9
  import sys
11
10
  import tempfile
12
11
  import typing
13
- import venv
14
12
 
15
13
  if typing.TYPE_CHECKING:
16
14
  from collections.abc import Callable
17
15
  from types import TracebackType
18
16
  from typing import Any
17
+ from .backends import VenvBackend
19
18
 
19
+ from .backends import VenvBackendRegistry
20
20
 
21
- VERSION = "0.0.1dev1"
21
+ VERSION = "0.0.2dev0"
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
  logger.setLevel(logging.DEBUG)
25
25
 
26
-
27
- VENV_ROOT = pathlib.Path(tempfile.gettempdir()) / __name__.split(".")[0] / "venvs"
26
+ VENV_ROOT = pathlib.Path(
27
+ os.environ.get("PYDEPINJECT_VENV_ROOT", None)
28
+ or pathlib.Path(tempfile.gettempdir()) / __name__.split(".")[0] / "venvs"
29
+ )
28
30
  logger.debug("VENV_ROOT: %s", VENV_ROOT)
29
31
 
32
+ VENV_BACKENDS = "|".join(VenvBackendRegistry.get_backends())
33
+ logger.debug("VENV_BACKENDS: %s", VENV_BACKENDS)
34
+
30
35
 
31
36
  def is_requirements_satisfied(*packages: str):
32
37
  """Check if the requirements are already satisfied. Return None if it cannot be determined."""
33
38
  try:
34
- if "pkg_resources" in sys.modules:
35
- del sys.modules["pkg_resources"]
36
- import pkg_resources
39
+ from importlib.metadata import PackageNotFoundError, distribution
40
+
41
+ from packaging.requirements import Requirement
37
42
  except ImportError:
38
- logger.debug(
39
- "pkg_resources not found. Cannot check if requirements are satisfied."
40
- )
41
- return None
43
+ try:
44
+ from importlib.metadata import PackageNotFoundError, distribution
45
+
46
+ from packaging.requirements import Requirement
47
+ except ImportError:
48
+ logger.warning(
49
+ "importlib.metadata and packaging not found. Cannot check if requirements are satisfied."
50
+ )
51
+ return None
42
52
 
43
53
  try:
44
54
  for package in packages:
45
55
  logger.debug("Checking package: %s", package)
46
- req = pkg_resources.Requirement.parse(package)
47
- pkg_resources.require(str(req))
48
- logger.debug("Requirement %s is satisfied", package)
56
+ req = Requirement(package)
57
+ try:
58
+ dist = distribution(req.name)
59
+ if dist.version not in req.specifier:
60
+ logger.debug(
61
+ "Requirement %s is not satisfied. Version conflict.", package
62
+ )
63
+ return False
64
+ logger.debug("Requirement %s is satisfied", package)
65
+ except PackageNotFoundError:
66
+ logger.debug(
67
+ "Requirement %s is not satisfied. Distribution not found.", package
68
+ )
69
+ return False
49
70
  return True
50
- except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
51
- logger.debug("Requirements %s are not satisfied. Return False", packages)
52
- return False
71
+ except Exception as e:
72
+ logger.warning("An error occurred while checking requirements: %s", str(e))
73
+ return None
53
74
 
54
75
 
55
76
  class RequirementManager:
@@ -60,6 +81,7 @@ class RequirementManager:
60
81
  *packages: str,
61
82
  venv_name: str | None = None,
62
83
  venv_root: pathlib.Path = VENV_ROOT,
84
+ venv_backend: str | None = None,
63
85
  recreate: bool = False,
64
86
  ephemeral: bool = False,
65
87
  ):
@@ -70,6 +92,7 @@ class RequirementManager:
70
92
  venv_name: The name of the virtual environment. If not provided,
71
93
  a unique name will be generated based on the package requirements.
72
94
  venv_root: The root directory for virtual environments.
95
+ venv_backend: The virtual environment backend to use. Defaults to $PYDEPINJECT_VENV_NAME or "uv|venv".
73
96
  recreate: If True, the virtual environment will be recreated if it exists.
74
97
  ephemeral: If True, the virtual environment will be deleted after use.
75
98
  """
@@ -80,10 +103,31 @@ class RequirementManager:
80
103
  self.original_syspath = sys.path.copy()
81
104
  self._venv_path = venv_root / self.venv_name if self.venv_name else None
82
105
  self._venv_root = venv_root
106
+
107
+ venv_backend = (
108
+ venv_backend
109
+ or os.environ.get("PYDEPINJECT_VENV_BACKEND", None)
110
+ or VENV_BACKENDS
111
+ )
112
+ self._venv_backends = [item.strip() for item in venv_backend.split("|")]
113
+ invalid_backends = set(self._venv_backends) - set(VENV_BACKENDS.split("|"))
114
+ if invalid_backends:
115
+ raise ValueError(f"Invalid venv_backend: {','.join(invalid_backends)}")
116
+
83
117
  self.ephemeral = ephemeral
84
118
  self.recreate = recreate
85
119
  self._activated = False
86
120
 
121
+ @property
122
+ def venv_backend_cls(self) -> type[VenvBackend]:
123
+ """Returns the virtual environment backend class."""
124
+ supported_backends = VenvBackendRegistry.get_supported_backends()
125
+ for backend in self._venv_backends:
126
+ if backend not in supported_backends:
127
+ continue
128
+ return supported_backends[backend]
129
+ raise ValueError("No supported venv backend found")
130
+
87
131
  @property
88
132
  def venv_path(self):
89
133
  if self._venv_path:
@@ -101,21 +145,11 @@ class RequirementManager:
101
145
  if self.venv_path.exists() and not self.recreate:
102
146
  return
103
147
  logger.debug("Creating virtualenv: %s", self.venv_path)
104
- venv.create(str(self.venv_path), with_pip=True, clear=self.recreate)
148
+ self.venv_backend_cls(self.venv_path).create(clear=self.recreate)
105
149
 
106
150
  def _install_packages(self):
107
151
  logger.info("Installing packages: %s", self.packages)
108
- pip_executable = pathlib.Path(self.venv_path) / "bin" / "pip"
109
- pip_args = [
110
- str(pip_executable),
111
- "install",
112
- "--quiet",
113
- "--no-python-version-warning",
114
- "--disable-pip-version-check",
115
- *self.packages,
116
- ]
117
- logger.debug("Running command: %s", " ".join(pip_args))
118
- subprocess.check_call(pip_args)
152
+ self.venv_backend_cls(self.venv_path).install(*self.packages)
119
153
 
120
154
  def _activate_venv(self):
121
155
  if is_requirements_satisfied(*self.packages):
@@ -131,11 +165,10 @@ class RequirementManager:
131
165
 
132
166
  self._create_virtualenv()
133
167
 
134
- venv_site_packages = (
135
- pathlib.Path(self.venv_path)
136
- / "lib"
137
- / f"python{sys.version_info.major}.{sys.version_info.minor}"
138
- / "site-packages"
168
+ venv_site_packages = pathlib.Path(self.venv_path).joinpath(
169
+ "lib",
170
+ f"python{sys.version_info.major}.{sys.version_info.minor}",
171
+ "site-packages",
139
172
  )
140
173
  os.environ["PYTHONPATH"] = str(venv_site_packages) + (
141
174
  os.pathsep + self.original_pythonpath if self.original_pythonpath else ""
@@ -194,7 +227,6 @@ class RequirementManager:
194
227
  @functools.wraps(func)
195
228
  def wrapper(*args: Any, **kwargs: Any):
196
229
  with self:
197
- print("returning func()")
198
230
  return func(*args, **kwargs)
199
231
 
200
232
  return wrapper
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import collections
4
+ import logging
5
+ import pathlib
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import typing
10
+ from typing import Protocol
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from collections.abc import MutableMapping
14
+
15
+ _PYTHON_BIN = sys.executable
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class VenvBackend(Protocol):
21
+ """Protocol for virtual environment backends."""
22
+
23
+ _path: pathlib.Path
24
+ _PRIORITY: int
25
+
26
+ def __init__(self, path: str | pathlib.Path): ...
27
+
28
+ @property
29
+ def name(self) -> str: ...
30
+
31
+ def create(self, clear: bool = False) -> None: ...
32
+
33
+ def install(self, *packages: str) -> None: ...
34
+
35
+ @classmethod
36
+ def is_supported(cls) -> bool: ...
37
+
38
+
39
+ class VenvBackendRegistry:
40
+ """Registry of virtual environment backends."""
41
+
42
+ _registry: MutableMapping[str, type[VenvBackend]] = {}
43
+
44
+ @classmethod
45
+ def register_backend(cls, backend_cls: type[VenvBackend]) -> None:
46
+ instance = backend_cls(
47
+ pathlib.Path()
48
+ ) # Create an instance to access the name property
49
+ cls._registry[instance.name] = backend_cls
50
+
51
+ @classmethod
52
+ def get_backend(cls, name: str) -> type[VenvBackend] | None:
53
+ return cls._registry.get(name)
54
+
55
+ @classmethod
56
+ def has_backend(cls, name: str) -> bool:
57
+ return name in cls._registry
58
+
59
+ @classmethod
60
+ def get_backends(cls) -> MutableMapping[str, type[VenvBackend]]:
61
+ result: MutableMapping[str, type[VenvBackend]] = collections.OrderedDict()
62
+ for name in sorted(cls._registry, key=lambda x: cls._registry[x]._PRIORITY): # pyright: ignore[reportPrivateUsage]
63
+ result[name] = cls._registry[name]
64
+ return result
65
+
66
+ @classmethod
67
+ def get_supported_backends(cls) -> dict[str, type[VenvBackend]]:
68
+ return {
69
+ name: backend_cls
70
+ for name, backend_cls in cls._registry.items()
71
+ if backend_cls.is_supported()
72
+ }
73
+
74
+
75
+ class VenvBackendVenv:
76
+ """Virtual environment backend using the venv module."""
77
+
78
+ _NAME = "venv"
79
+ _PRIORITY: int = 0
80
+
81
+ def __init__(self, path: str | pathlib.Path):
82
+ self._path = pathlib.Path(path)
83
+
84
+ @property
85
+ def name(self) -> str:
86
+ return self._NAME
87
+
88
+ @classmethod
89
+ def is_supported(cls) -> bool:
90
+ return True
91
+
92
+ def create(self, clear: bool = False) -> None:
93
+ clear_opt = ["--clear"] if clear else []
94
+ venv_args = [_PYTHON_BIN, "-m", "venv"] + clear_opt + [str(self._path)]
95
+ logger.debug("Running command: %s", " ".join(venv_args))
96
+ subprocess.check_call(venv_args)
97
+
98
+ def install(self, *packages: str) -> None:
99
+ if not self._path.exists():
100
+ raise FileNotFoundError(f"Virtual environment not found: {self._path}")
101
+
102
+ pip_executable = self._path / "bin" / "pip"
103
+ if not pip_executable.exists():
104
+ raise FileNotFoundError(f"pip executable not found: {pip_executable}")
105
+
106
+ pip_args = [
107
+ str(pip_executable),
108
+ "install",
109
+ "--quiet",
110
+ "--no-python-version-warning",
111
+ "--disable-pip-version-check",
112
+ "--upgrade",
113
+ *packages,
114
+ ]
115
+ logger.debug("Running command: %s", " ".join(pip_args))
116
+ subprocess.check_call(pip_args)
117
+
118
+
119
+ class VenvBackendUV:
120
+ """Virtual environment backend using the uv tool."""
121
+
122
+ _NAME = "uv"
123
+ _PRIORITY: int = 1
124
+ _CMD = "uv"
125
+
126
+ def __init__(self, path: str | pathlib.Path):
127
+ self._path = pathlib.Path(path)
128
+
129
+ @property
130
+ def name(self) -> str:
131
+ return self._NAME
132
+
133
+ @classmethod
134
+ def is_supported(cls) -> bool:
135
+ return bool(shutil.which(cls._CMD))
136
+
137
+ def create(self, clear: bool = False) -> None:
138
+ if clear and self._path.exists():
139
+ shutil.rmtree(self._path)
140
+ subprocess.check_call([
141
+ f"{self._CMD}",
142
+ "venv",
143
+ "--python",
144
+ _PYTHON_BIN,
145
+ self._path.as_posix(),
146
+ ])
147
+
148
+ def install(self, *packages: str) -> None:
149
+ if not self._path.exists():
150
+ raise FileNotFoundError(f"Virtual environment not found: {self._path}")
151
+ pip_args = [
152
+ f"{self._CMD}",
153
+ "pip",
154
+ "install",
155
+ "--python",
156
+ (self._path / "bin" / "python").as_posix(),
157
+ "--quiet",
158
+ "--upgrade",
159
+ *packages,
160
+ ]
161
+ logger.debug("Running command: %s", " ".join(pip_args))
162
+ subprocess.check_call(pip_args)
163
+
164
+
165
+ # Register backends
166
+ VenvBackendRegistry.register_backend(VenvBackendVenv)
167
+ VenvBackendRegistry.register_backend(VenvBackendUV)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pydepinject
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.2.dev0
4
4
  Summary: A package to dynamically inject requirements into a virtual environment.
5
5
  Author: pydepinject
6
6
  License: MIT
@@ -21,6 +21,8 @@ Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
+ Requires-Dist: packaging>=22.0
25
+ Requires-Dist: typing_extensions
24
26
  Provides-Extra: lint
25
27
  Requires-Dist: ruff==0.4.7; extra == "lint"
26
28
  Requires-Dist: pyright==1.1.365; extra == "lint"
@@ -28,6 +30,7 @@ Requires-Dist: isort==5.13.2; extra == "lint"
28
30
  Provides-Extra: test
29
31
  Requires-Dist: pytest==8.2.1; extra == "test"
30
32
  Requires-Dist: pytest-cov==5.0.0; extra == "test"
33
+ Requires-Dist: pytest-xdist==3.6.1; extra == "test"
31
34
  Provides-Extra: build
32
35
  Requires-Dist: check-manifest==0.49; extra == "build"
33
36
  Requires-Dist: build==1.2.1; extra == "build"
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  Readme.md
4
4
  pyproject.toml
5
5
  src/pydepinject/__init__.py
6
+ src/pydepinject/backends.py
6
7
  src/pydepinject.egg-info/PKG-INFO
7
8
  src/pydepinject.egg-info/SOURCES.txt
8
9
  src/pydepinject.egg-info/dependency_links.txt
@@ -13,4 +14,5 @@ src/requirementmanager.egg-info/SOURCES.txt
13
14
  src/requirementmanager.egg-info/dependency_links.txt
14
15
  src/requirementmanager.egg-info/requires.txt
15
16
  src/requirementmanager.egg-info/top_level.txt
17
+ tests/conftest.py
16
18
  tests/test_pydepinject.py
@@ -1,3 +1,5 @@
1
+ packaging>=22.0
2
+ typing_extensions
1
3
 
2
4
  [build]
3
5
  check-manifest==0.49
@@ -13,3 +15,4 @@ isort==5.13.2
13
15
  [test]
14
16
  pytest==8.2.1
15
17
  pytest-cov==5.0.0
18
+ pytest-xdist==3.6.1
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+
5
+ import pytest
6
+
7
+ PROJECT_DIR = pathlib.Path(__file__).parent.parent
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def check_test_leftovers():
12
+ """Checks if the test left any files in the project directory."""
13
+ items_before = list(PROJECT_DIR.iterdir())
14
+ yield
15
+ items_after = list(PROJECT_DIR.iterdir())
16
+ new_items = set(items_after) - set(items_before)
17
+ new_items = {item for item in new_items if not item.name.startswith(".coverage")}
18
+ if new_items:
19
+ pytest.fail(f"New items in the project directory: {new_items}")
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
4
+
3
5
  import pytest
4
6
 
5
7
  from pydepinject import requires # noqa: E402
@@ -13,14 +15,19 @@ def venv_root(tmp_path):
13
15
  return path
14
16
 
15
17
 
18
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
16
19
  @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
17
- def test_decorator(venv_root, ephemeral):
20
+ def test_decorator(venv_root, ephemeral, backend):
18
21
  assert not list(venv_root.iterdir())
19
22
 
20
23
  with pytest.raises(ImportError):
21
24
  import six
22
25
 
23
- @requires("six", venv_root=venv_root, ephemeral=ephemeral)
26
+ requires_ = functools.partial(
27
+ requires, venv_root=venv_root, ephemeral=ephemeral, venv_backend=backend
28
+ )
29
+
30
+ @requires_("six")
24
31
  def examplefn():
25
32
  print("examplefn")
26
33
  import six
@@ -37,15 +44,24 @@ def test_decorator(venv_root, ephemeral):
37
44
  assert len(list(venv_root.iterdir())) == 1
38
45
 
39
46
 
47
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
40
48
  @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
41
- def test_venv_name_predefined(venv_root, ephemeral):
49
+ def test_venv_name_predefined(venv_root, ephemeral, backend):
42
50
  assert not list(venv_root.iterdir())
43
51
 
44
52
  venv_name = "test_venv_name_predefined"
45
53
  with pytest.raises(ImportError):
46
54
  import six
47
55
 
48
- @requires("six", venv_root=venv_root, venv_name=venv_name, ephemeral=ephemeral)
56
+ requires_ = functools.partial(
57
+ requires,
58
+ venv_root=venv_root,
59
+ venv_name=venv_name,
60
+ ephemeral=ephemeral,
61
+ venv_backend=backend,
62
+ )
63
+
64
+ @requires_("six")
49
65
  def examplefn():
50
66
  print("examplefn")
51
67
  import six
@@ -60,7 +76,7 @@ def test_venv_name_predefined(venv_root, ephemeral):
60
76
  assert (venv_root / venv_name).exists() is (not ephemeral)
61
77
  assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
62
78
 
63
- with requires("six", venv_root=venv_root, venv_name=venv_name, ephemeral=ephemeral):
79
+ with requires_("six"):
64
80
  import six
65
81
 
66
82
  assert six.__version__
@@ -73,8 +89,9 @@ def test_venv_name_predefined(venv_root, ephemeral):
73
89
  assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
74
90
 
75
91
 
92
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
76
93
  @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
77
- def test_venv_name_predefined_env(venv_root, monkeypatch, ephemeral):
94
+ def test_venv_name_predefined_env(venv_root, monkeypatch, ephemeral, backend):
78
95
  assert not list(venv_root.iterdir())
79
96
 
80
97
  venv_name = "test_venv_name_predefined_env"
@@ -83,7 +100,11 @@ def test_venv_name_predefined_env(venv_root, monkeypatch, ephemeral):
83
100
  with pytest.raises(ImportError):
84
101
  import six
85
102
 
86
- @requires("six", venv_root=venv_root, ephemeral=ephemeral)
103
+ requires_ = functools.partial(
104
+ requires, venv_root=venv_root, ephemeral=ephemeral, venv_backend=backend
105
+ )
106
+
107
+ @requires_("six")
87
108
  def examplefn():
88
109
  print("examplefn")
89
110
  import six
@@ -98,7 +119,7 @@ def test_venv_name_predefined_env(venv_root, monkeypatch, ephemeral):
98
119
  assert (venv_root / venv_name).exists() is (not ephemeral)
99
120
  assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
100
121
 
101
- with requires("six", venv_root=venv_root, ephemeral=ephemeral):
122
+ with requires_("six"):
102
123
  import six
103
124
 
104
125
  assert six.__version__
@@ -111,14 +132,18 @@ def test_venv_name_predefined_env(venv_root, monkeypatch, ephemeral):
111
132
  assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
112
133
 
113
134
 
135
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
114
136
  @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
115
- def test_context_manager(venv_root, ephemeral):
137
+ def test_context_manager(venv_root, ephemeral, backend):
116
138
  assert not list(venv_root.iterdir())
117
139
 
118
140
  with pytest.raises(ImportError):
119
141
  import six
120
142
 
121
- with requires("six", venv_root=venv_root, ephemeral=ephemeral):
143
+ requires_ = functools.partial(
144
+ requires, venv_root=venv_root, ephemeral=ephemeral, venv_backend=backend
145
+ )
146
+ with requires_("six"):
122
147
  import six
123
148
 
124
149
  assert six.__version__
@@ -132,13 +157,14 @@ def test_context_manager(venv_root, ephemeral):
132
157
  assert len(list(venv_root.iterdir())) == 1
133
158
 
134
159
 
135
- def test_function_call(venv_root):
160
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
161
+ def test_function_call(venv_root, backend):
136
162
  assert not list(venv_root.iterdir())
137
163
 
138
164
  with pytest.raises(ImportError):
139
165
  import six
140
166
 
141
- requires_instance = requires("six", venv_root=venv_root)
167
+ requires_instance = requires("six", venv_root=venv_root, venv_backend=backend)
142
168
  requires_instance()
143
169
  import six
144
170
 
@@ -151,10 +177,11 @@ def test_function_call(venv_root):
151
177
  assert len(list(venv_root.iterdir())) == 1
152
178
 
153
179
 
154
- def test_no_installs(venv_root):
180
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
181
+ def test_no_installs(venv_root, backend):
155
182
  assert not list(venv_root.iterdir())
156
183
 
157
- @requires("pytest", venv_root=venv_root, ephemeral=False)
184
+ @requires("pytest", venv_root=venv_root, ephemeral=False, venv_backend=backend)
158
185
  def examplefn():
159
186
  print("examplefn")
160
187
 
@@ -162,17 +189,22 @@ def test_no_installs(venv_root):
162
189
  assert not list(venv_root.iterdir())
163
190
 
164
191
 
165
- def test_reuse_venv(venv_root):
192
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
193
+ def test_reuse_venv(venv_root, backend):
166
194
  assert not list(venv_root.iterdir())
167
195
 
168
- @requires("six", venv_root=venv_root, ephemeral=False)
196
+ requires_ = functools.partial(
197
+ requires, venv_root=venv_root, ephemeral=False, venv_backend=backend
198
+ )
199
+
200
+ @requires_("six")
169
201
  def examplea():
170
202
  import six
171
203
 
172
204
  assert six.__version__
173
205
  examplea.called = True
174
206
 
175
- @requires("six", venv_root=venv_root, ephemeral=False)
207
+ @requires_("six")
176
208
  def exampleb():
177
209
  import six
178
210
 
@@ -192,19 +224,28 @@ def test_reuse_venv(venv_root):
192
224
  assert len(list(venv_root.iterdir())) == 1
193
225
 
194
226
 
195
- def test_one_venv_multiple_packages(venv_root):
227
+ @pytest.mark.parametrize("backend", ["uv", "venv"], ids=["uv", "venv"])
228
+ def test_one_venv_multiple_packages(venv_root, backend):
196
229
  assert not list(venv_root.iterdir())
197
230
 
198
231
  venv_name = "test_one_venv_multiple_packages"
199
232
 
200
- @requires("six", venv_root=venv_root, venv_name=venv_name, ephemeral=False)
233
+ requires_ = functools.partial(
234
+ requires,
235
+ venv_root=venv_root,
236
+ venv_name=venv_name,
237
+ ephemeral=False,
238
+ venv_backend=backend,
239
+ )
240
+
241
+ @requires_("six")
201
242
  def examplefn():
202
243
  import six
203
244
 
204
245
  assert six.__version__
205
246
  assert (venv_root / venv_name).exists()
206
247
 
207
- @requires("pyparsing", venv_root=venv_root, venv_name=venv_name, ephemeral=False)
248
+ @requires_("pyparsing")
208
249
  def examplefn2():
209
250
  import pyparsing
210
251
 
@@ -1,3 +0,0 @@
1
- graft src
2
- exclude Dockerfile noxfile.py tasks.py scripts/*
3
- global-exclude *~ *.py[cod] *.so .devcontainer/*