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.
@@ -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,3 @@
1
+ graft src
2
+ exclude Dockerfile noxfile.py tasks.py scripts/*
3
+ global-exclude *~ *.py[cod] *.so .devcontainer/*
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,15 @@
1
+
2
+ [build]
3
+ check-manifest==0.49
4
+ build==1.2.1
5
+ wheel==0.43.0
6
+ setuptools==70.0.0
7
+
8
+ [lint]
9
+ ruff==0.4.7
10
+ pyright==1.1.365
11
+ isort==5.13.2
12
+
13
+ [test]
14
+ pytest==8.2.1
15
+ pytest-cov==5.0.0
@@ -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,11 @@
1
+
2
+ [dev]
3
+ ruff
4
+ pytest
5
+ flake8
6
+ black
7
+ pyright
8
+ twine
9
+ wheel
10
+ setuptools
11
+ pre-commit
@@ -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