pydepinject 0.0.1.dev0__tar.gz → 0.0.1.dev2__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 (18) hide show
  1. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/MANIFEST.in +1 -1
  2. {pydepinject-0.0.1.dev0/src/pydepinject.egg-info → pydepinject-0.0.1.dev2}/PKG-INFO +40 -1
  3. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/Readme.md +37 -0
  4. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/pyproject.toml +8 -2
  5. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/pydepinject/__init__.py +31 -15
  6. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2/src/pydepinject.egg-info}/PKG-INFO +40 -1
  7. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/pydepinject.egg-info/requires.txt +2 -0
  8. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/tests/test_pydepinject.py +80 -0
  9. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/LICENSE +0 -0
  10. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/setup.cfg +0 -0
  11. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/pydepinject.egg-info/SOURCES.txt +0 -0
  12. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/pydepinject.egg-info/dependency_links.txt +0 -0
  13. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/pydepinject.egg-info/top_level.txt +0 -0
  14. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/requirementmanager.egg-info/PKG-INFO +0 -0
  15. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/requirementmanager.egg-info/SOURCES.txt +0 -0
  16. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/requirementmanager.egg-info/dependency_links.txt +0 -0
  17. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/requirementmanager.egg-info/requires.txt +0 -0
  18. {pydepinject-0.0.1.dev0 → pydepinject-0.0.1.dev2}/src/requirementmanager.egg-info/top_level.txt +0 -0
@@ -1,3 +1,3 @@
1
1
  graft src
2
2
  exclude Dockerfile noxfile.py tasks.py scripts/*
3
- global-exclude *~ *.py[cod] *.so .devcontainer/*
3
+ 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.dev0
3
+ Version: 0.0.1.dev2
4
4
  Summary: A package to dynamically inject requirements into a virtual environment.
5
5
  Author: pydepinject
6
6
  License: MIT
@@ -21,6 +21,7 @@ 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
24
25
  Provides-Extra: lint
25
26
  Requires-Dist: ruff==0.4.7; extra == "lint"
26
27
  Requires-Dist: pyright==1.1.365; extra == "lint"
@@ -28,6 +29,7 @@ Requires-Dist: isort==5.13.2; extra == "lint"
28
29
  Provides-Extra: test
29
30
  Requires-Dist: pytest==8.2.1; extra == "test"
30
31
  Requires-Dist: pytest-cov==5.0.0; extra == "test"
32
+ Requires-Dist: pytest-xdist==3.6.1; extra == "test"
31
33
  Provides-Extra: build
32
34
  Requires-Dist: check-manifest==0.49; extra == "build"
33
35
  Requires-Dist: build==1.2.1; extra == "build"
@@ -86,6 +88,43 @@ with requires("requests", "numpy"):
86
88
  print(np.__version__)
87
89
  ```
88
90
 
91
+ ### Virtual Environment with specific name
92
+
93
+ The `requires` can create a virtual environment with a specific name:
94
+
95
+ ```python
96
+ @requires("requests", venv_name="myenv")
97
+ def my_function():
98
+ import requests
99
+ print(requests.__version__)
100
+
101
+
102
+ with requires("pylint", venv_name="myenv"):
103
+ import pylint
104
+ print(pylint.__version__)
105
+ import requests # This is also available here because it was installed in the same virtual environment
106
+ print(requests.__version__)
107
+
108
+
109
+ # The virtual environment name can also be set as PYDEPINJECT_VENV_NAME environment variable
110
+ import os
111
+ os.environ["PYDEPINJECT_VENV_NAME"] = "myenv"
112
+
113
+ @requires("requests")
114
+ def my_function():
115
+ import requests
116
+ print(requests.__version__)
117
+
118
+
119
+ with requires("pylint"):
120
+ import pylint
121
+ print(pylint.__version__)
122
+ import requests # This is also available here because it was installed in the same virtual environment
123
+ print(requests.__version__)
124
+ ```
125
+
126
+
127
+
89
128
  ### Reusable Virtual Environments
90
129
 
91
130
  The `requires` can create named virtual environments and reuse them across multiple functions or code blocks:
@@ -50,6 +50,43 @@ with requires("requests", "numpy"):
50
50
  print(np.__version__)
51
51
  ```
52
52
 
53
+ ### Virtual Environment with specific name
54
+
55
+ The `requires` can create a virtual environment with a specific name:
56
+
57
+ ```python
58
+ @requires("requests", venv_name="myenv")
59
+ def my_function():
60
+ import requests
61
+ print(requests.__version__)
62
+
63
+
64
+ with requires("pylint", venv_name="myenv"):
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
+ # The virtual environment name can also be set as PYDEPINJECT_VENV_NAME environment variable
72
+ import os
73
+ os.environ["PYDEPINJECT_VENV_NAME"] = "myenv"
74
+
75
+ @requires("requests")
76
+ def my_function():
77
+ import requests
78
+ print(requests.__version__)
79
+
80
+
81
+ with requires("pylint"):
82
+ import pylint
83
+ print(pylint.__version__)
84
+ import requests # This is also available here because it was installed in the same virtual environment
85
+ print(requests.__version__)
86
+ ```
87
+
88
+
89
+
53
90
  ### Reusable Virtual Environments
54
91
 
55
92
  The `requires` can create named virtual environments and reuse them across multiple functions or code blocks:
@@ -15,6 +15,9 @@ 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
+ ]
18
21
  classifiers = [
19
22
  "Development Status :: 4 - Beta",
20
23
  "Intended Audience :: Developers",
@@ -42,6 +45,7 @@ lint = [
42
45
  test = [
43
46
  "pytest==8.2.1",
44
47
  "pytest-cov==5.0.0",
48
+ "pytest-xdist==3.6.1",
45
49
  ]
46
50
  build = [
47
51
  "check-manifest==0.49",
@@ -62,7 +66,8 @@ log_level = "DEBUG"
62
66
 
63
67
  [tool.pytest.ini_options]
64
68
  pythonpath = "src"
65
-
69
+ addopts = "--durations=10 -n auto"
70
+ # Estimated test times: 74s with auto (2), 76s with 6 workers, 113se with no workers.
66
71
 
67
72
  [tool.ruff]
68
73
  preview = true
@@ -126,7 +131,8 @@ ignore = []
126
131
  uv = {version = "0.2.5"}
127
132
  invoke = {version = "2.2.0", preinstall = ["tomli"]}
128
133
  nox = {version = "2024.4.15"}
129
- twine = {version = "5.1.1"}
134
+ twine = {version = "5.1.0"}
130
135
 
131
136
  [tool.custom.ci]
132
137
  python_versions = ["3.9", "3.10", "3.11", "3.12"]
138
+ packaging_versions = ["22", "23", "24"]
@@ -18,7 +18,7 @@ if typing.TYPE_CHECKING:
18
18
  from typing import Any
19
19
 
20
20
 
21
- VERSION = "0.0.1dev"
21
+ VERSION = "0.0.1dev2"
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
  logger.setLevel(logging.DEBUG)
@@ -31,25 +31,41 @@ logger.debug("VENV_ROOT: %s", VENV_ROOT)
31
31
  def is_requirements_satisfied(*packages: str):
32
32
  """Check if the requirements are already satisfied. Return None if it cannot be determined."""
33
33
  try:
34
- if "pkg_resources" in sys.modules:
35
- del sys.modules["pkg_resources"]
36
- import pkg_resources
34
+ from importlib.metadata import PackageNotFoundError, distribution
35
+
36
+ from packaging.requirements import Requirement
37
37
  except ImportError:
38
- logger.debug(
39
- "pkg_resources not found. Cannot check if requirements are satisfied."
40
- )
41
- return None
38
+ try:
39
+ from importlib.metadata import PackageNotFoundError, distribution
40
+
41
+ from packaging.requirements import Requirement
42
+ except ImportError:
43
+ logger.warning(
44
+ "importlib.metadata and packaging not found. Cannot check if requirements are satisfied."
45
+ )
46
+ return None
42
47
 
43
48
  try:
44
49
  for package in packages:
45
50
  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)
51
+ req = Requirement(package)
52
+ try:
53
+ dist = distribution(req.name)
54
+ if dist.version not in req.specifier:
55
+ logger.debug(
56
+ "Requirement %s is not satisfied. Version conflict.", package
57
+ )
58
+ return False
59
+ logger.debug("Requirement %s is satisfied", package)
60
+ except PackageNotFoundError:
61
+ logger.debug(
62
+ "Requirement %s is not satisfied. Distribution not found.", package
63
+ )
64
+ return False
49
65
  return True
50
- except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
51
- logger.debug("Requirements %s are not satisfied. Return False", packages)
52
- return False
66
+ except Exception as e:
67
+ logger.warning("An error occurred while checking requirements: %s", str(e))
68
+ return None
53
69
 
54
70
 
55
71
  class RequirementManager:
@@ -74,7 +90,7 @@ class RequirementManager:
74
90
  ephemeral: If True, the virtual environment will be deleted after use.
75
91
  """
76
92
  self.packages = packages
77
- self.venv_name = venv_name
93
+ self.venv_name = venv_name or os.environ.get("PYDEPINJECT_VENV_NAME", "")
78
94
  self.original_pythonpath = os.environ.get("PYTHONPATH", "")
79
95
  self.original_path = os.environ.get("PATH", "")
80
96
  self.original_syspath = sys.path.copy()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pydepinject
3
- Version: 0.0.1.dev0
3
+ Version: 0.0.1.dev2
4
4
  Summary: A package to dynamically inject requirements into a virtual environment.
5
5
  Author: pydepinject
6
6
  License: MIT
@@ -21,6 +21,7 @@ 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
24
25
  Provides-Extra: lint
25
26
  Requires-Dist: ruff==0.4.7; extra == "lint"
26
27
  Requires-Dist: pyright==1.1.365; extra == "lint"
@@ -28,6 +29,7 @@ Requires-Dist: isort==5.13.2; extra == "lint"
28
29
  Provides-Extra: test
29
30
  Requires-Dist: pytest==8.2.1; extra == "test"
30
31
  Requires-Dist: pytest-cov==5.0.0; extra == "test"
32
+ Requires-Dist: pytest-xdist==3.6.1; extra == "test"
31
33
  Provides-Extra: build
32
34
  Requires-Dist: check-manifest==0.49; extra == "build"
33
35
  Requires-Dist: build==1.2.1; extra == "build"
@@ -86,6 +88,43 @@ with requires("requests", "numpy"):
86
88
  print(np.__version__)
87
89
  ```
88
90
 
91
+ ### Virtual Environment with specific name
92
+
93
+ The `requires` can create a virtual environment with a specific name:
94
+
95
+ ```python
96
+ @requires("requests", venv_name="myenv")
97
+ def my_function():
98
+ import requests
99
+ print(requests.__version__)
100
+
101
+
102
+ with requires("pylint", venv_name="myenv"):
103
+ import pylint
104
+ print(pylint.__version__)
105
+ import requests # This is also available here because it was installed in the same virtual environment
106
+ print(requests.__version__)
107
+
108
+
109
+ # The virtual environment name can also be set as PYDEPINJECT_VENV_NAME environment variable
110
+ import os
111
+ os.environ["PYDEPINJECT_VENV_NAME"] = "myenv"
112
+
113
+ @requires("requests")
114
+ def my_function():
115
+ import requests
116
+ print(requests.__version__)
117
+
118
+
119
+ with requires("pylint"):
120
+ import pylint
121
+ print(pylint.__version__)
122
+ import requests # This is also available here because it was installed in the same virtual environment
123
+ print(requests.__version__)
124
+ ```
125
+
126
+
127
+
89
128
  ### Reusable Virtual Environments
90
129
 
91
130
  The `requires` can create named virtual environments and reuse them across multiple functions or code blocks:
@@ -1,3 +1,4 @@
1
+ packaging~=22.0
1
2
 
2
3
  [build]
3
4
  check-manifest==0.49
@@ -13,3 +14,4 @@ isort==5.13.2
13
14
  [test]
14
15
  pytest==8.2.1
15
16
  pytest-cov==5.0.0
17
+ pytest-xdist==3.6.1
@@ -37,6 +37,80 @@ def test_decorator(venv_root, ephemeral):
37
37
  assert len(list(venv_root.iterdir())) == 1
38
38
 
39
39
 
40
+ @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
41
+ def test_venv_name_predefined(venv_root, ephemeral):
42
+ assert not list(venv_root.iterdir())
43
+
44
+ venv_name = "test_venv_name_predefined"
45
+ with pytest.raises(ImportError):
46
+ import six
47
+
48
+ @requires("six", venv_root=venv_root, venv_name=venv_name, ephemeral=ephemeral)
49
+ def examplefn():
50
+ print("examplefn")
51
+ import six
52
+
53
+ assert six.__version__
54
+ assert (venv_root / venv_name).exists()
55
+
56
+ examplefn()
57
+ with pytest.raises(ImportError):
58
+ import six
59
+
60
+ assert (venv_root / venv_name).exists() is (not ephemeral)
61
+ assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
62
+
63
+ with requires("six", venv_root=venv_root, venv_name=venv_name, ephemeral=ephemeral):
64
+ import six
65
+
66
+ assert six.__version__
67
+ assert (venv_root / venv_name).exists()
68
+
69
+ with pytest.raises(ImportError):
70
+ import six
71
+
72
+ assert (venv_root / venv_name).exists() is (not ephemeral)
73
+ assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
74
+
75
+
76
+ @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
77
+ def test_venv_name_predefined_env(venv_root, monkeypatch, ephemeral):
78
+ assert not list(venv_root.iterdir())
79
+
80
+ venv_name = "test_venv_name_predefined_env"
81
+ monkeypatch.setenv("PYDEPINJECT_VENV_NAME", venv_name)
82
+
83
+ with pytest.raises(ImportError):
84
+ import six
85
+
86
+ @requires("six", venv_root=venv_root, ephemeral=ephemeral)
87
+ def examplefn():
88
+ print("examplefn")
89
+ import six
90
+
91
+ assert six.__version__
92
+ assert (venv_root / venv_name).exists()
93
+
94
+ examplefn()
95
+ with pytest.raises(ImportError):
96
+ import six
97
+
98
+ assert (venv_root / venv_name).exists() is (not ephemeral)
99
+ assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
100
+
101
+ with requires("six", venv_root=venv_root, ephemeral=ephemeral):
102
+ import six
103
+
104
+ assert six.__version__
105
+ assert (venv_root / venv_name).exists()
106
+
107
+ with pytest.raises(ImportError):
108
+ import six
109
+
110
+ assert (venv_root / venv_name).exists() is (not ephemeral)
111
+ assert len(list(venv_root.iterdir())) == (1 if not ephemeral else 0)
112
+
113
+
40
114
  @pytest.mark.parametrize("ephemeral", [True, False], ids=["ephemeral", "non-ephemeral"])
41
115
  def test_context_manager(venv_root, ephemeral):
42
116
  assert not list(venv_root.iterdir())
@@ -74,6 +148,7 @@ def test_function_call(venv_root):
74
148
  requires_instance._deactivate_venv()
75
149
  with pytest.raises(ImportError):
76
150
  import six
151
+ assert len(list(venv_root.iterdir())) == 1
77
152
 
78
153
 
79
154
  def test_no_installs(venv_root):
@@ -127,6 +202,7 @@ def test_one_venv_multiple_packages(venv_root):
127
202
  import six
128
203
 
129
204
  assert six.__version__
205
+ assert (venv_root / venv_name).exists()
130
206
 
131
207
  @requires("pyparsing", venv_root=venv_root, venv_name=venv_name, ephemeral=False)
132
208
  def examplefn2():
@@ -137,10 +213,14 @@ def test_one_venv_multiple_packages(venv_root):
137
213
  import six
138
214
 
139
215
  assert six.__version__
216
+ assert (venv_root / venv_name).exists()
140
217
 
141
218
  examplefn()
142
219
  examplefn2()
143
220
 
221
+ # Still exists as ephemeral is False.
222
+ assert (venv_root / venv_name).exists()
223
+
144
224
  with pytest.raises(ImportError):
145
225
  import six
146
226
  with pytest.raises(ImportError):