pydepinject 0.0.1.dev0__py3-none-any.whl

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,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,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: build
25
+ Requires-Dist: check-manifest ==0.49 ; extra == 'build'
26
+ Requires-Dist: build ==1.2.1 ; extra == 'build'
27
+ Requires-Dist: wheel ==0.43.0 ; extra == 'build'
28
+ Requires-Dist: setuptools ==70.0.0 ; extra == 'build'
29
+ Provides-Extra: lint
30
+ Requires-Dist: ruff ==0.4.7 ; extra == 'lint'
31
+ Requires-Dist: pyright ==1.1.365 ; extra == 'lint'
32
+ Requires-Dist: isort ==5.13.2 ; extra == 'lint'
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest ==8.2.1 ; extra == 'test'
35
+ Requires-Dist: pytest-cov ==5.0.0 ; extra == 'test'
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,6 @@
1
+ pydepinject/__init__.py,sha256=mjbRTs9SFE-Cmwn12hCABwdMb5z-Dz7DFuaTBxbMdTA,6856
2
+ pydepinject-0.0.1.dev0.dist-info/LICENSE,sha256=Rwfkv5kSPGRiBn_2Cb7lNyo3Dvc02a5jqMmJnvgnW7s,1086
3
+ pydepinject-0.0.1.dev0.dist-info/METADATA,sha256=xSRjvFHdi9HsMDqwUb_Ku8MYHgCuza2oCN21A1JjNok,4442
4
+ pydepinject-0.0.1.dev0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
5
+ pydepinject-0.0.1.dev0.dist-info/top_level.txt,sha256=VdrZmvJZGzKrHnAs17D7cBjpyIrE5HRVaQjPB13xHew,12
6
+ pydepinject-0.0.1.dev0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pydepinject