pydepinject 0.0.1.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.
- pydepinject-0.0.1.dev0/LICENSE +9 -0
- pydepinject-0.0.1.dev0/MANIFEST.in +3 -0
- pydepinject-0.0.1.dev0/PKG-INFO +138 -0
- pydepinject-0.0.1.dev0/Readme.md +102 -0
- pydepinject-0.0.1.dev0/pyproject.toml +132 -0
- pydepinject-0.0.1.dev0/setup.cfg +4 -0
- pydepinject-0.0.1.dev0/src/pydepinject/__init__.py +203 -0
- pydepinject-0.0.1.dev0/src/pydepinject.egg-info/PKG-INFO +138 -0
- pydepinject-0.0.1.dev0/src/pydepinject.egg-info/SOURCES.txt +16 -0
- pydepinject-0.0.1.dev0/src/pydepinject.egg-info/dependency_links.txt +1 -0
- pydepinject-0.0.1.dev0/src/pydepinject.egg-info/requires.txt +15 -0
- pydepinject-0.0.1.dev0/src/pydepinject.egg-info/top_level.txt +1 -0
- pydepinject-0.0.1.dev0/src/requirementmanager.egg-info/PKG-INFO +32 -0
- pydepinject-0.0.1.dev0/src/requirementmanager.egg-info/SOURCES.txt +7 -0
- pydepinject-0.0.1.dev0/src/requirementmanager.egg-info/dependency_links.txt +1 -0
- pydepinject-0.0.1.dev0/src/requirementmanager.egg-info/requires.txt +11 -0
- pydepinject-0.0.1.dev0/src/requirementmanager.egg-info/top_level.txt +1 -0
- pydepinject-0.0.1.dev0/tests/test_pydepinject.py +149 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2024 pydepinject
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
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.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pydepinject
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: A package to dynamically inject requirements into a virtual environment.
|
|
5
|
+
Author: pydepinject
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/pydepinject/pydepinject
|
|
8
|
+
Project-URL: documentation, https://github.com/pydepinject/pydepinject
|
|
9
|
+
Project-URL: repository, https://github.com/pydepinject/pydepinject
|
|
10
|
+
Keywords: virtualenv,requirements,dependency management
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: lint
|
|
25
|
+
Requires-Dist: ruff==0.4.7; extra == "lint"
|
|
26
|
+
Requires-Dist: pyright==1.1.365; extra == "lint"
|
|
27
|
+
Requires-Dist: isort==5.13.2; extra == "lint"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest==8.2.1; extra == "test"
|
|
30
|
+
Requires-Dist: pytest-cov==5.0.0; extra == "test"
|
|
31
|
+
Provides-Extra: build
|
|
32
|
+
Requires-Dist: check-manifest==0.49; extra == "build"
|
|
33
|
+
Requires-Dist: build==1.2.1; extra == "build"
|
|
34
|
+
Requires-Dist: wheel==0.43.0; extra == "build"
|
|
35
|
+
Requires-Dist: setuptools==70.0.0; extra == "build"
|
|
36
|
+
|
|
37
|
+
# Requirement Manager
|
|
38
|
+
|
|
39
|
+
This project provides a `RequirementManager` (`requires` is an alias) class to manage Python package requirements using virtual environments. It can be used as a decorator or context manager to ensure specific packages are installed and available during the execution of a function or code block.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- Automatically creates and manages virtual environments.
|
|
44
|
+
- Checks if the required packages are already installed.
|
|
45
|
+
- Installs packages if they are not already available.
|
|
46
|
+
- Supports ephemeral virtual environments that are deleted after use.
|
|
47
|
+
- Can be used as a decorator or context manager.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
`pip install pydepinject`
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Decorator
|
|
57
|
+
|
|
58
|
+
To use the `requires` as a decorator, simply decorate your function with the required packages:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pydepinject import requires
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@requires("requests", "numpy")
|
|
65
|
+
def my_function():
|
|
66
|
+
import requests
|
|
67
|
+
import numpy as np
|
|
68
|
+
print(requests.__version__)
|
|
69
|
+
print(np.__version__)
|
|
70
|
+
|
|
71
|
+
my_function()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Context Manager
|
|
75
|
+
|
|
76
|
+
You can also use the `requires` as a context manager:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pydepinject import requires
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
with requires("requests", "numpy"):
|
|
83
|
+
import requests
|
|
84
|
+
import numpy as np
|
|
85
|
+
print(requests.__version__)
|
|
86
|
+
print(np.__version__)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Reusable Virtual Environments
|
|
90
|
+
|
|
91
|
+
The `requires` can create named virtual environments and reuse them across multiple functions or code blocks:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
@requires("requests", venv_name="myenv", ephemeral=False)
|
|
95
|
+
def my_function():
|
|
96
|
+
import requests
|
|
97
|
+
print(requests.__version__)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
with requires("pylint", venv_name="myenv", ephemeral=False):
|
|
101
|
+
import pylint
|
|
102
|
+
print(pylint.__version__)
|
|
103
|
+
import requests # This is also available here because it was installed in the same virtual environment
|
|
104
|
+
print(requests.__version__)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Managing Virtual Environments
|
|
108
|
+
|
|
109
|
+
The `requires` can automatically delete ephemeral virtual environments after use. This is useful when you want to ensure that the virtual environment is clean and does not persist after the function or code block completes:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
@requires("requests", venv_name="myenv", ephemeral=True)
|
|
113
|
+
def my_function():
|
|
114
|
+
import requests
|
|
115
|
+
print(requests.__version__)
|
|
116
|
+
|
|
117
|
+
my_function()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Logging
|
|
121
|
+
|
|
122
|
+
The `requires` uses the `logging` module to provide debug information. By default, it logs to the console at the DEBUG level. You can adjust the logging configuration as needed.
|
|
123
|
+
|
|
124
|
+
## Unit Tests
|
|
125
|
+
|
|
126
|
+
Unit tests are provided to verify the functionality of the `requires`. The tests use `pytest` and cover various scenarios including decorator usage, context manager usage, ephemeral environments, and more.
|
|
127
|
+
|
|
128
|
+
### Running Tests
|
|
129
|
+
|
|
130
|
+
To run the unit tests, ensure you have `pytest` installed, and then execute the following command:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Requirement Manager
|
|
2
|
+
|
|
3
|
+
This project provides a `RequirementManager` (`requires` is an alias) class to manage Python package requirements using virtual environments. It can be used as a decorator or context manager to ensure specific packages are installed and available during the execution of a function or code block.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Automatically creates and manages virtual environments.
|
|
8
|
+
- Checks if the required packages are already installed.
|
|
9
|
+
- Installs packages if they are not already available.
|
|
10
|
+
- Supports ephemeral virtual environments that are deleted after use.
|
|
11
|
+
- Can be used as a decorator or context manager.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
`pip install pydepinject`
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Decorator
|
|
21
|
+
|
|
22
|
+
To use the `requires` as a decorator, simply decorate your function with the required packages:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pydepinject import requires
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@requires("requests", "numpy")
|
|
29
|
+
def my_function():
|
|
30
|
+
import requests
|
|
31
|
+
import numpy as np
|
|
32
|
+
print(requests.__version__)
|
|
33
|
+
print(np.__version__)
|
|
34
|
+
|
|
35
|
+
my_function()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Context Manager
|
|
39
|
+
|
|
40
|
+
You can also use the `requires` as a context manager:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pydepinject import requires
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
with requires("requests", "numpy"):
|
|
47
|
+
import requests
|
|
48
|
+
import numpy as np
|
|
49
|
+
print(requests.__version__)
|
|
50
|
+
print(np.__version__)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Reusable Virtual Environments
|
|
54
|
+
|
|
55
|
+
The `requires` can create named virtual environments and reuse them across multiple functions or code blocks:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
@requires("requests", venv_name="myenv", ephemeral=False)
|
|
59
|
+
def my_function():
|
|
60
|
+
import requests
|
|
61
|
+
print(requests.__version__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
with requires("pylint", venv_name="myenv", ephemeral=False):
|
|
65
|
+
import pylint
|
|
66
|
+
print(pylint.__version__)
|
|
67
|
+
import requests # This is also available here because it was installed in the same virtual environment
|
|
68
|
+
print(requests.__version__)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Managing Virtual Environments
|
|
72
|
+
|
|
73
|
+
The `requires` can automatically delete ephemeral virtual environments after use. This is useful when you want to ensure that the virtual environment is clean and does not persist after the function or code block completes:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
@requires("requests", venv_name="myenv", ephemeral=True)
|
|
77
|
+
def my_function():
|
|
78
|
+
import requests
|
|
79
|
+
print(requests.__version__)
|
|
80
|
+
|
|
81
|
+
my_function()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Logging
|
|
85
|
+
|
|
86
|
+
The `requires` uses the `logging` module to provide debug information. By default, it logs to the console at the DEBUG level. You can adjust the logging configuration as needed.
|
|
87
|
+
|
|
88
|
+
## Unit Tests
|
|
89
|
+
|
|
90
|
+
Unit tests are provided to verify the functionality of the `requires`. The tests use `pytest` and cover various scenarios including decorator usage, context manager usage, ephemeral environments, and more.
|
|
91
|
+
|
|
92
|
+
### Running Tests
|
|
93
|
+
|
|
94
|
+
To run the unit tests, ensure you have `pytest` installed, and then execute the following command:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools >= 61",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "pydepinject"
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
description = "A package to dynamically inject requirements into a virtual environment."
|
|
11
|
+
readme = "Readme.md"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name="pydepinject" }
|
|
14
|
+
]
|
|
15
|
+
license = { text="MIT" }
|
|
16
|
+
keywords = ["virtualenv", "requirements", "dependency management"]
|
|
17
|
+
requires-python = ">=3.9"
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
homepage = "https://github.com/pydepinject/pydepinject"
|
|
33
|
+
documentation = "https://github.com/pydepinject/pydepinject"
|
|
34
|
+
repository = "https://github.com/pydepinject/pydepinject"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
lint = [
|
|
38
|
+
"ruff==0.4.7",
|
|
39
|
+
"pyright==1.1.365",
|
|
40
|
+
"isort==5.13.2",
|
|
41
|
+
]
|
|
42
|
+
test = [
|
|
43
|
+
"pytest==8.2.1",
|
|
44
|
+
"pytest-cov==5.0.0",
|
|
45
|
+
]
|
|
46
|
+
build = [
|
|
47
|
+
"check-manifest==0.49",
|
|
48
|
+
"build==1.2.1",
|
|
49
|
+
"wheel==0.43.0",
|
|
50
|
+
"setuptools==70.0.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools]
|
|
54
|
+
package-dir = {"" = "src"}
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.dynamic]
|
|
57
|
+
version = {attr = "pydepinject.VERSION"}
|
|
58
|
+
|
|
59
|
+
[tool.pytest]
|
|
60
|
+
log_cli = true
|
|
61
|
+
log_level = "DEBUG"
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
pythonpath = "src"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
preview = true
|
|
69
|
+
extend-exclude = [
|
|
70
|
+
"/..*"
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint.per-file-ignores]
|
|
74
|
+
"tests/test_pydepinject.py" = ["F401", "F811"]
|
|
75
|
+
|
|
76
|
+
[tool.pyright]
|
|
77
|
+
include = ["src"]
|
|
78
|
+
strict = ["src"]
|
|
79
|
+
|
|
80
|
+
deprecateTypingAliases = true
|
|
81
|
+
reportCallInDefaultInitializer = true
|
|
82
|
+
reportImplicitOverride = true
|
|
83
|
+
reportImplicitStringConcatenation = true
|
|
84
|
+
reportImportCycles = true
|
|
85
|
+
reportMissingSuperCall = false
|
|
86
|
+
reportPropertyTypeMismatch = true
|
|
87
|
+
reportShadowedImports = true
|
|
88
|
+
reportUninitializedInstanceVariable = true
|
|
89
|
+
reportUnnecessaryTypeIgnoreComment = true
|
|
90
|
+
reportUnusedCallResult = false
|
|
91
|
+
|
|
92
|
+
[tool.isort]
|
|
93
|
+
profile = "black"
|
|
94
|
+
add_imports = ["from __future__ import annotations"]
|
|
95
|
+
known_first_party = ["pydepinject"]
|
|
96
|
+
skip_gitignore = true
|
|
97
|
+
src_paths = [
|
|
98
|
+
"src",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[tool.coverage.run]
|
|
102
|
+
branch = true
|
|
103
|
+
dynamic_context = "test_function"
|
|
104
|
+
cover_pylib = false
|
|
105
|
+
data_file = ".coverage"
|
|
106
|
+
parallel = false
|
|
107
|
+
relative_files = true
|
|
108
|
+
|
|
109
|
+
[tool.coverage.report]
|
|
110
|
+
exclude_also = [
|
|
111
|
+
"if TYPE_CHECKING:",
|
|
112
|
+
"if __name__ == .__main__.:",
|
|
113
|
+
]
|
|
114
|
+
format = "text"
|
|
115
|
+
show_missing = true
|
|
116
|
+
|
|
117
|
+
[tool.coverage.html]
|
|
118
|
+
directory = "htmlcov"
|
|
119
|
+
show_contexts = true
|
|
120
|
+
title = "Coverage report for pydepinject"
|
|
121
|
+
|
|
122
|
+
[tool.check-manifest]
|
|
123
|
+
ignore = []
|
|
124
|
+
|
|
125
|
+
[tool.custom.pipxtools]
|
|
126
|
+
uv = {version = "0.2.5"}
|
|
127
|
+
invoke = {version = "2.2.0", preinstall = ["tomli"]}
|
|
128
|
+
nox = {version = "2024.4.15"}
|
|
129
|
+
twine = {version = "5.1.1"}
|
|
130
|
+
|
|
131
|
+
[tool.custom.ci]
|
|
132
|
+
python_versions = ["3.9", "3.10", "3.11", "3.12"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import typing
|
|
13
|
+
import venv
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from types import TracebackType
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
VERSION = "0.0.1dev"
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
logger.setLevel(logging.DEBUG)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
VENV_ROOT = pathlib.Path(tempfile.gettempdir()) / __name__.split(".")[0] / "venvs"
|
|
28
|
+
logger.debug("VENV_ROOT: %s", VENV_ROOT)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_requirements_satisfied(*packages: str):
|
|
32
|
+
"""Check if the requirements are already satisfied. Return None if it cannot be determined."""
|
|
33
|
+
try:
|
|
34
|
+
if "pkg_resources" in sys.modules:
|
|
35
|
+
del sys.modules["pkg_resources"]
|
|
36
|
+
import pkg_resources
|
|
37
|
+
except ImportError:
|
|
38
|
+
logger.debug(
|
|
39
|
+
"pkg_resources not found. Cannot check if requirements are satisfied."
|
|
40
|
+
)
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
for package in packages:
|
|
45
|
+
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)
|
|
49
|
+
return True
|
|
50
|
+
except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
|
|
51
|
+
logger.debug("Requirements %s are not satisfied. Return False", packages)
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RequirementManager:
|
|
56
|
+
"""A decorator and context manager to manage Python package requirements."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*packages: str,
|
|
61
|
+
venv_name: str | None = None,
|
|
62
|
+
venv_root: pathlib.Path = VENV_ROOT,
|
|
63
|
+
recreate: bool = False,
|
|
64
|
+
ephemeral: bool = False,
|
|
65
|
+
):
|
|
66
|
+
"""Initialize the RequirementManager.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
*packages: A list of package requirements.
|
|
70
|
+
venv_name: The name of the virtual environment. If not provided,
|
|
71
|
+
a unique name will be generated based on the package requirements.
|
|
72
|
+
venv_root: The root directory for virtual environments.
|
|
73
|
+
recreate: If True, the virtual environment will be recreated if it exists.
|
|
74
|
+
ephemeral: If True, the virtual environment will be deleted after use.
|
|
75
|
+
"""
|
|
76
|
+
self.packages = packages
|
|
77
|
+
self.venv_name = venv_name
|
|
78
|
+
self.original_pythonpath = os.environ.get("PYTHONPATH", "")
|
|
79
|
+
self.original_path = os.environ.get("PATH", "")
|
|
80
|
+
self.original_syspath = sys.path.copy()
|
|
81
|
+
self._venv_path = venv_root / self.venv_name if self.venv_name else None
|
|
82
|
+
self._venv_root = venv_root
|
|
83
|
+
self.ephemeral = ephemeral
|
|
84
|
+
self.recreate = recreate
|
|
85
|
+
self._activated = False
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def venv_path(self):
|
|
89
|
+
if self._venv_path:
|
|
90
|
+
"""Returns a path to the virtual environment. If not set, a unique path is generated."""
|
|
91
|
+
return self._venv_path
|
|
92
|
+
# Create a unique hash for the package requirements
|
|
93
|
+
reqs_str = ",".join(self.packages)
|
|
94
|
+
reqs_hash = hashlib.md5(reqs_str.encode()).hexdigest()
|
|
95
|
+
|
|
96
|
+
self._venv_path = self._venv_root / reqs_hash
|
|
97
|
+
return self._venv_path
|
|
98
|
+
|
|
99
|
+
def _create_virtualenv(self):
|
|
100
|
+
"""Create a virtual environment if it does not exist."""
|
|
101
|
+
if self.venv_path.exists() and not self.recreate:
|
|
102
|
+
return
|
|
103
|
+
logger.debug("Creating virtualenv: %s", self.venv_path)
|
|
104
|
+
venv.create(str(self.venv_path), with_pip=True, clear=self.recreate)
|
|
105
|
+
|
|
106
|
+
def _install_packages(self):
|
|
107
|
+
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)
|
|
119
|
+
|
|
120
|
+
def _activate_venv(self):
|
|
121
|
+
if is_requirements_satisfied(*self.packages):
|
|
122
|
+
logger.debug(
|
|
123
|
+
"Requirements %s already satisfied. No need to create venv",
|
|
124
|
+
self.packages,
|
|
125
|
+
)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
self.original_pythonpath = os.environ.get("PYTHONPATH", "")
|
|
129
|
+
self.original_path = os.environ.get("PATH", "")
|
|
130
|
+
self.original_syspath = sys.path.copy()
|
|
131
|
+
|
|
132
|
+
self._create_virtualenv()
|
|
133
|
+
|
|
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"
|
|
139
|
+
)
|
|
140
|
+
os.environ["PYTHONPATH"] = str(venv_site_packages) + (
|
|
141
|
+
os.pathsep + self.original_pythonpath if self.original_pythonpath else ""
|
|
142
|
+
)
|
|
143
|
+
os.environ["PATH"] = (
|
|
144
|
+
str(pathlib.Path(self.venv_path) / "bin") + os.pathsep + self.original_path
|
|
145
|
+
)
|
|
146
|
+
sys.path.insert(0, str(venv_site_packages))
|
|
147
|
+
self._activated = True
|
|
148
|
+
if is_requirements_satisfied(*self.packages):
|
|
149
|
+
logger.debug(
|
|
150
|
+
"Requirements %s already satisfied within %s",
|
|
151
|
+
self.packages,
|
|
152
|
+
self.venv_path,
|
|
153
|
+
)
|
|
154
|
+
return self
|
|
155
|
+
self._install_packages()
|
|
156
|
+
|
|
157
|
+
def _deactivate_venv(self):
|
|
158
|
+
if not self._activated:
|
|
159
|
+
return
|
|
160
|
+
os.environ["PATH"] = self.original_path
|
|
161
|
+
os.environ["PYTHONPATH"] = self.original_pythonpath
|
|
162
|
+
sys.path = self.original_syspath
|
|
163
|
+
# Cleanup imported cached modules from the temporary venv.
|
|
164
|
+
venv_imports: set[str] = set()
|
|
165
|
+
for name, module in sys.modules.items():
|
|
166
|
+
module_path = getattr(module, "__file__", None)
|
|
167
|
+
if module_path and pathlib.Path(module_path).is_relative_to(self.venv_path):
|
|
168
|
+
venv_imports.add(name)
|
|
169
|
+
for name in venv_imports:
|
|
170
|
+
del sys.modules[name]
|
|
171
|
+
self._activated = False
|
|
172
|
+
if self.ephemeral:
|
|
173
|
+
logger.debug("Deleting ephemeral venv: %s", self.venv_path)
|
|
174
|
+
shutil.rmtree(self.venv_path)
|
|
175
|
+
|
|
176
|
+
def __enter__(self):
|
|
177
|
+
self._activate_venv()
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def __exit__(
|
|
181
|
+
self,
|
|
182
|
+
exctype: type[BaseException] | None,
|
|
183
|
+
excinst: BaseException | None,
|
|
184
|
+
exctb: TracebackType | None,
|
|
185
|
+
) -> None:
|
|
186
|
+
del exctype, excinst, exctb
|
|
187
|
+
self._deactivate_venv()
|
|
188
|
+
|
|
189
|
+
def __call__(self, func: Callable[..., Any] | None = None):
|
|
190
|
+
if func is None:
|
|
191
|
+
self._activate_venv()
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
@functools.wraps(func)
|
|
195
|
+
def wrapper(*args: Any, **kwargs: Any):
|
|
196
|
+
with self:
|
|
197
|
+
print("returning func()")
|
|
198
|
+
return func(*args, **kwargs)
|
|
199
|
+
|
|
200
|
+
return wrapper
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
requires = RequirementManager
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pydepinject
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: A package to dynamically inject requirements into a virtual environment.
|
|
5
|
+
Author: pydepinject
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/pydepinject/pydepinject
|
|
8
|
+
Project-URL: documentation, https://github.com/pydepinject/pydepinject
|
|
9
|
+
Project-URL: repository, https://github.com/pydepinject/pydepinject
|
|
10
|
+
Keywords: virtualenv,requirements,dependency management
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: lint
|
|
25
|
+
Requires-Dist: ruff==0.4.7; extra == "lint"
|
|
26
|
+
Requires-Dist: pyright==1.1.365; extra == "lint"
|
|
27
|
+
Requires-Dist: isort==5.13.2; extra == "lint"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest==8.2.1; extra == "test"
|
|
30
|
+
Requires-Dist: pytest-cov==5.0.0; extra == "test"
|
|
31
|
+
Provides-Extra: build
|
|
32
|
+
Requires-Dist: check-manifest==0.49; extra == "build"
|
|
33
|
+
Requires-Dist: build==1.2.1; extra == "build"
|
|
34
|
+
Requires-Dist: wheel==0.43.0; extra == "build"
|
|
35
|
+
Requires-Dist: setuptools==70.0.0; extra == "build"
|
|
36
|
+
|
|
37
|
+
# Requirement Manager
|
|
38
|
+
|
|
39
|
+
This project provides a `RequirementManager` (`requires` is an alias) class to manage Python package requirements using virtual environments. It can be used as a decorator or context manager to ensure specific packages are installed and available during the execution of a function or code block.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- Automatically creates and manages virtual environments.
|
|
44
|
+
- Checks if the required packages are already installed.
|
|
45
|
+
- Installs packages if they are not already available.
|
|
46
|
+
- Supports ephemeral virtual environments that are deleted after use.
|
|
47
|
+
- Can be used as a decorator or context manager.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
`pip install pydepinject`
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Decorator
|
|
57
|
+
|
|
58
|
+
To use the `requires` as a decorator, simply decorate your function with the required packages:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pydepinject import requires
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@requires("requests", "numpy")
|
|
65
|
+
def my_function():
|
|
66
|
+
import requests
|
|
67
|
+
import numpy as np
|
|
68
|
+
print(requests.__version__)
|
|
69
|
+
print(np.__version__)
|
|
70
|
+
|
|
71
|
+
my_function()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Context Manager
|
|
75
|
+
|
|
76
|
+
You can also use the `requires` as a context manager:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pydepinject import requires
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
with requires("requests", "numpy"):
|
|
83
|
+
import requests
|
|
84
|
+
import numpy as np
|
|
85
|
+
print(requests.__version__)
|
|
86
|
+
print(np.__version__)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Reusable Virtual Environments
|
|
90
|
+
|
|
91
|
+
The `requires` can create named virtual environments and reuse them across multiple functions or code blocks:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
@requires("requests", venv_name="myenv", ephemeral=False)
|
|
95
|
+
def my_function():
|
|
96
|
+
import requests
|
|
97
|
+
print(requests.__version__)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
with requires("pylint", venv_name="myenv", ephemeral=False):
|
|
101
|
+
import pylint
|
|
102
|
+
print(pylint.__version__)
|
|
103
|
+
import requests # This is also available here because it was installed in the same virtual environment
|
|
104
|
+
print(requests.__version__)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Managing Virtual Environments
|
|
108
|
+
|
|
109
|
+
The `requires` can automatically delete ephemeral virtual environments after use. This is useful when you want to ensure that the virtual environment is clean and does not persist after the function or code block completes:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
@requires("requests", venv_name="myenv", ephemeral=True)
|
|
113
|
+
def my_function():
|
|
114
|
+
import requests
|
|
115
|
+
print(requests.__version__)
|
|
116
|
+
|
|
117
|
+
my_function()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Logging
|
|
121
|
+
|
|
122
|
+
The `requires` uses the `logging` module to provide debug information. By default, it logs to the console at the DEBUG level. You can adjust the logging configuration as needed.
|
|
123
|
+
|
|
124
|
+
## Unit Tests
|
|
125
|
+
|
|
126
|
+
Unit tests are provided to verify the functionality of the `requires`. The tests use `pytest` and cover various scenarios including decorator usage, context manager usage, ephemeral environments, and more.
|
|
127
|
+
|
|
128
|
+
### Running Tests
|
|
129
|
+
|
|
130
|
+
To run the unit tests, ensure you have `pytest` installed, and then execute the following command:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
Readme.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/pydepinject/__init__.py
|
|
6
|
+
src/pydepinject.egg-info/PKG-INFO
|
|
7
|
+
src/pydepinject.egg-info/SOURCES.txt
|
|
8
|
+
src/pydepinject.egg-info/dependency_links.txt
|
|
9
|
+
src/pydepinject.egg-info/requires.txt
|
|
10
|
+
src/pydepinject.egg-info/top_level.txt
|
|
11
|
+
src/requirementmanager.egg-info/PKG-INFO
|
|
12
|
+
src/requirementmanager.egg-info/SOURCES.txt
|
|
13
|
+
src/requirementmanager.egg-info/dependency_links.txt
|
|
14
|
+
src/requirementmanager.egg-info/requires.txt
|
|
15
|
+
src/requirementmanager.egg-info/top_level.txt
|
|
16
|
+
tests/test_pydepinject.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pydepinject
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: requirementmanager
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: A package to dynamically inject requirements into a virtual environment.
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
Maintainer-email: Your Name <your.email@example.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: homepage, https://github.com/yourusername/requirementmanager
|
|
9
|
+
Project-URL: documentation, https://github.com/yourusername/requirementmanager
|
|
10
|
+
Project-URL: repository, https://github.com/yourusername/requirementmanager
|
|
11
|
+
Keywords: virtualenv,requirements,dependency management
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Description-Content-Type: text/x-rst
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Requires-Dist: flake8; extra == "dev"
|
|
27
|
+
Requires-Dist: black; extra == "dev"
|
|
28
|
+
Requires-Dist: pyright; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Requires-Dist: wheel; extra == "dev"
|
|
31
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
32
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/pydepinject/__init__.py
|
|
3
|
+
src/requirementmanager.egg-info/PKG-INFO
|
|
4
|
+
src/requirementmanager.egg-info/SOURCES.txt
|
|
5
|
+
src/requirementmanager.egg-info/dependency_links.txt
|
|
6
|
+
src/requirementmanager.egg-info/requires.txt
|
|
7
|
+
src/requirementmanager.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pydepinject
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pydepinject import requires # noqa: E402
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def venv_root(tmp_path):
|
|
10
|
+
"""Return the root directory for virtual environments."""
|
|
11
|
+
path = tmp_path / "venvs"
|
|
12
|
+
path.mkdir()
|
|
13
|
+
return path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
|
|
17
|
+
def test_decorator(venv_root, ephemeral):
|
|
18
|
+
assert not list(venv_root.iterdir())
|
|
19
|
+
|
|
20
|
+
with pytest.raises(ImportError):
|
|
21
|
+
import six
|
|
22
|
+
|
|
23
|
+
@requires("six", venv_root=venv_root, ephemeral=ephemeral)
|
|
24
|
+
def examplefn():
|
|
25
|
+
print("examplefn")
|
|
26
|
+
import six
|
|
27
|
+
|
|
28
|
+
assert six.__version__
|
|
29
|
+
|
|
30
|
+
examplefn()
|
|
31
|
+
with pytest.raises(ImportError):
|
|
32
|
+
import six
|
|
33
|
+
|
|
34
|
+
if ephemeral:
|
|
35
|
+
assert not list(venv_root.iterdir())
|
|
36
|
+
else:
|
|
37
|
+
assert len(list(venv_root.iterdir())) == 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
|
|
41
|
+
def test_context_manager(venv_root, ephemeral):
|
|
42
|
+
assert not list(venv_root.iterdir())
|
|
43
|
+
|
|
44
|
+
with pytest.raises(ImportError):
|
|
45
|
+
import six
|
|
46
|
+
|
|
47
|
+
with requires("six", venv_root=venv_root, ephemeral=ephemeral):
|
|
48
|
+
import six
|
|
49
|
+
|
|
50
|
+
assert six.__version__
|
|
51
|
+
|
|
52
|
+
with pytest.raises(ImportError):
|
|
53
|
+
import six
|
|
54
|
+
|
|
55
|
+
if ephemeral:
|
|
56
|
+
assert not list(venv_root.iterdir())
|
|
57
|
+
else:
|
|
58
|
+
assert len(list(venv_root.iterdir())) == 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_function_call(venv_root):
|
|
62
|
+
assert not list(venv_root.iterdir())
|
|
63
|
+
|
|
64
|
+
with pytest.raises(ImportError):
|
|
65
|
+
import six
|
|
66
|
+
|
|
67
|
+
requires_instance = requires("six", venv_root=venv_root)
|
|
68
|
+
requires_instance()
|
|
69
|
+
import six
|
|
70
|
+
|
|
71
|
+
assert six.__version__
|
|
72
|
+
assert len(list(venv_root.iterdir())) == 1
|
|
73
|
+
|
|
74
|
+
requires_instance._deactivate_venv()
|
|
75
|
+
with pytest.raises(ImportError):
|
|
76
|
+
import six
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_no_installs(venv_root):
|
|
80
|
+
assert not list(venv_root.iterdir())
|
|
81
|
+
|
|
82
|
+
@requires("pytest", venv_root=venv_root, ephemeral=False)
|
|
83
|
+
def examplefn():
|
|
84
|
+
print("examplefn")
|
|
85
|
+
|
|
86
|
+
examplefn()
|
|
87
|
+
assert not list(venv_root.iterdir())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_reuse_venv(venv_root):
|
|
91
|
+
assert not list(venv_root.iterdir())
|
|
92
|
+
|
|
93
|
+
@requires("six", venv_root=venv_root, ephemeral=False)
|
|
94
|
+
def examplea():
|
|
95
|
+
import six
|
|
96
|
+
|
|
97
|
+
assert six.__version__
|
|
98
|
+
examplea.called = True
|
|
99
|
+
|
|
100
|
+
@requires("six", venv_root=venv_root, ephemeral=False)
|
|
101
|
+
def exampleb():
|
|
102
|
+
import six
|
|
103
|
+
|
|
104
|
+
assert six.__version__
|
|
105
|
+
global exampleb_called
|
|
106
|
+
exampleb.called = True
|
|
107
|
+
|
|
108
|
+
examplea()
|
|
109
|
+
with pytest.raises(ImportError):
|
|
110
|
+
import six
|
|
111
|
+
|
|
112
|
+
exampleb()
|
|
113
|
+
with pytest.raises(ImportError):
|
|
114
|
+
import six
|
|
115
|
+
|
|
116
|
+
assert examplea.called is exampleb.called is True
|
|
117
|
+
assert len(list(venv_root.iterdir())) == 1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_one_venv_multiple_packages(venv_root):
|
|
121
|
+
assert not list(venv_root.iterdir())
|
|
122
|
+
|
|
123
|
+
venv_name = "test_one_venv_multiple_packages"
|
|
124
|
+
|
|
125
|
+
@requires("six", venv_root=venv_root, venv_name=venv_name, ephemeral=False)
|
|
126
|
+
def examplefn():
|
|
127
|
+
import six
|
|
128
|
+
|
|
129
|
+
assert six.__version__
|
|
130
|
+
|
|
131
|
+
@requires("pyparsing", venv_root=venv_root, venv_name=venv_name, ephemeral=False)
|
|
132
|
+
def examplefn2():
|
|
133
|
+
import pyparsing
|
|
134
|
+
|
|
135
|
+
assert pyparsing.__version__
|
|
136
|
+
|
|
137
|
+
import six
|
|
138
|
+
|
|
139
|
+
assert six.__version__
|
|
140
|
+
|
|
141
|
+
examplefn()
|
|
142
|
+
examplefn2()
|
|
143
|
+
|
|
144
|
+
with pytest.raises(ImportError):
|
|
145
|
+
import six
|
|
146
|
+
with pytest.raises(ImportError):
|
|
147
|
+
import pyparsing
|
|
148
|
+
|
|
149
|
+
assert len(list(venv_root.iterdir())) == 1
|