reyk 0.0.1__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.
- reyk/__init__.py +0 -0
- reyk/caller_finder.py +52 -0
- reyk/isolator.py +46 -0
- reyk/stdlib_finder.py +28 -0
- reyk/vendor_importer.py +292 -0
- reyk/vendored_sys_modules.py +108 -0
- reyk/version.py +1 -0
- reyk-0.0.1.dist-info/METADATA +76 -0
- reyk-0.0.1.dist-info/RECORD +11 -0
- reyk-0.0.1.dist-info/WHEEL +4 -0
- reyk-0.0.1.dist-info/licenses/LICENSE +21 -0
reyk/__init__.py
ADDED
|
File without changes
|
reyk/caller_finder.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from reyk.stdlib_finder import is_part_of_stdlib
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
MY_PACKAGE_NAME = __package__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NoCallerOutsideLibFoundError(RuntimeError): ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class StackFrame:
|
|
18
|
+
filename: Path
|
|
19
|
+
module_name: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_caller_frame_outside_reyk() -> StackFrame:
|
|
23
|
+
for frame in _iterate_over_stack():
|
|
24
|
+
if is_part_of_stdlib(frame.module_name):
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
if frame.module_name == MY_PACKAGE_NAME or frame.module_name.startswith(f"{MY_PACKAGE_NAME}."):
|
|
28
|
+
# This function
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
return frame
|
|
32
|
+
|
|
33
|
+
raise NoCallerOutsideLibFoundError("Failed to find caller outside builtin")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _iterate_over_stack() -> Iterator[StackFrame]:
|
|
37
|
+
current_frame = sys._getframe(1) # noqa: SLF001
|
|
38
|
+
while current_frame is not None:
|
|
39
|
+
yield StackFrame(
|
|
40
|
+
filename=Path(current_frame.f_code.co_filename),
|
|
41
|
+
module_name=current_frame.f_globals["__name__"],
|
|
42
|
+
)
|
|
43
|
+
current_frame = current_frame.f_back
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_caller_part_of_library(library_name: str) -> bool:
|
|
47
|
+
try:
|
|
48
|
+
caller_frame = get_caller_frame_outside_reyk()
|
|
49
|
+
except NoCallerOutsideLibFoundError:
|
|
50
|
+
return False
|
|
51
|
+
caller_module_name = caller_frame.module_name
|
|
52
|
+
return (caller_module_name == library_name) or (caller_module_name.startswith(f"{library_name}."))
|
reyk/isolator.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import importlib
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, cast
|
|
6
|
+
|
|
7
|
+
from reyk.caller_finder import get_caller_frame_outside_reyk
|
|
8
|
+
from reyk.vendor_importer import BuiltinsImporter, VendorImporter
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def isolate_package(package_path: Optional[Path] = None, vendorized_libs_directory_import_path: str = "libs") -> None:
|
|
14
|
+
vendor_importer = create_vendor_importer(package_path, vendorized_libs_directory_import_path)
|
|
15
|
+
LOGGER.debug(f"Isolating library: {vendor_importer.package_name} ({vendorized_libs_directory_import_path=})")
|
|
16
|
+
vendor_importer.install()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_vendor_importer(
|
|
20
|
+
package_path: Optional[Path] = None,
|
|
21
|
+
vendorized_libs_directory_import_path: str = "libs",
|
|
22
|
+
) -> VendorImporter:
|
|
23
|
+
"""
|
|
24
|
+
Isolates a package dependencies - everything imported from the specified package_path
|
|
25
|
+
will prefer to import libraries inside the specified vendorized_libs_directory_import_path
|
|
26
|
+
instead of the default PYTHONPATH/site-packages.
|
|
27
|
+
The package_path can be unspecified/None to default to the caller's parent package.
|
|
28
|
+
vendorized_libs_directory_import_path should be the path to import the dependencies
|
|
29
|
+
from the package.
|
|
30
|
+
For example if the dependencies are in 'libs' it should be 'libs' as well as we need to perform:
|
|
31
|
+
`import libs` but if it's a subdirectory like 'my/libs' it should be 'my.libs' for
|
|
32
|
+
`import my.libs`.
|
|
33
|
+
"""
|
|
34
|
+
if package_path is None:
|
|
35
|
+
package_path = get_caller_frame_outside_reyk().filename.parent
|
|
36
|
+
|
|
37
|
+
package_name = package_path.name
|
|
38
|
+
vendorized_libs_path = package_path / vendorized_libs_directory_import_path
|
|
39
|
+
return VendorImporter(
|
|
40
|
+
package_name,
|
|
41
|
+
vendorized_libs_directory_import_path,
|
|
42
|
+
vendorized_libs_path,
|
|
43
|
+
# Cast as for some reason the definition of __import__ thinks fromlist is not nullable
|
|
44
|
+
cast(BuiltinsImporter, builtins.__import__),
|
|
45
|
+
importlib.import_module,
|
|
46
|
+
)
|
reyk/stdlib_finder.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from functools import cache
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@cache
|
|
9
|
+
def list_stdlib_modules() -> list[str]:
|
|
10
|
+
return list(_list_stdlib_modules_iter())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _list_stdlib_modules_iter() -> Iterator[str]:
|
|
14
|
+
standard_lib_dir = Path(os.__file__).parent
|
|
15
|
+
for std_lib_file in standard_lib_dir.iterdir():
|
|
16
|
+
if _is_package(std_lib_file) or (std_lib_file.suffix == ".py"):
|
|
17
|
+
yield std_lib_file.stem
|
|
18
|
+
|
|
19
|
+
yield from sys.builtin_module_names
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_package(path: Path) -> bool:
|
|
23
|
+
return path.is_dir() and (path / "__init__.py").exists()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_part_of_stdlib(module_name: str) -> bool:
|
|
27
|
+
library_name, _, _ = module_name.partition(".")
|
|
28
|
+
return library_name in list_stdlib_modules()
|
reyk/vendor_importer.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import functools
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
8
|
+
from importlib.metadata import Distribution, DistributionFinder, MetadataPathFinder
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
from typing import Optional, Protocol
|
|
12
|
+
|
|
13
|
+
from reyk.caller_finder import is_caller_part_of_library
|
|
14
|
+
from reyk.stdlib_finder import is_part_of_stdlib
|
|
15
|
+
from reyk.vendored_sys_modules import VendoredSysModules
|
|
16
|
+
|
|
17
|
+
LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
RELATIVE_IMPORT_PREFIX = "."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BuiltinsImporter(Protocol):
|
|
23
|
+
def __call__(
|
|
24
|
+
self,
|
|
25
|
+
name: str,
|
|
26
|
+
globals: Optional[Mapping[str, object]] = None,
|
|
27
|
+
locals: Optional[Mapping[str, object]] = None,
|
|
28
|
+
fromlist: Optional[Sequence[str]] = (),
|
|
29
|
+
level: int = 0,
|
|
30
|
+
) -> ModuleType: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ImportLibImporter(Protocol):
|
|
34
|
+
def __call__(
|
|
35
|
+
self,
|
|
36
|
+
name: str,
|
|
37
|
+
package: Optional[str] = None,
|
|
38
|
+
) -> ModuleType: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VendorImporter(DistributionFinder):
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
package_name: str,
|
|
45
|
+
vendorized_libs_relative_import_path: str,
|
|
46
|
+
vendorized_libs_path: Path,
|
|
47
|
+
original_builtins_import_method: BuiltinsImporter,
|
|
48
|
+
original_importlib_import_method: ImportLibImporter,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.package_name = package_name
|
|
51
|
+
self.vendorized_libs_relative_import_path = vendorized_libs_relative_import_path
|
|
52
|
+
self.vendorized_libs_path = vendorized_libs_path
|
|
53
|
+
self._original_builtins_import_method = original_builtins_import_method
|
|
54
|
+
self._original_importlib_import_method = original_importlib_import_method
|
|
55
|
+
self._sys_modules_wrapper = VendoredSysModules(sys.modules, self.package_name, self.vendor_prefix)
|
|
56
|
+
|
|
57
|
+
def builtins_import_override(
|
|
58
|
+
self,
|
|
59
|
+
name: str,
|
|
60
|
+
globals: Optional[Mapping[str, object]] = None,
|
|
61
|
+
locals: Optional[Mapping[str, object]] = None,
|
|
62
|
+
fromlist: Optional[Sequence[str]] = (),
|
|
63
|
+
level: int = 0,
|
|
64
|
+
) -> ModuleType:
|
|
65
|
+
# Note: ALL import calls should be in this function as to not increase the call stack significantly
|
|
66
|
+
if not self._should_import_vendorized(name):
|
|
67
|
+
# If importing a module which shouldn't be vendorized
|
|
68
|
+
# we need to exit the context to ensure we don't retrieve
|
|
69
|
+
# a vendorized module
|
|
70
|
+
self._sys_modules_wrapper.remove_vendored_sys_modules()
|
|
71
|
+
return self._original_builtins_import_method(name, globals, locals, fromlist, level)
|
|
72
|
+
|
|
73
|
+
vendorized_import_path = self._vendor_import(name)
|
|
74
|
+
self._sys_modules_wrapper.install_vendored_sys_modules()
|
|
75
|
+
imported_vendorized: bool
|
|
76
|
+
try:
|
|
77
|
+
LOGGER.debug(f"Importing: {vendorized_import_path}")
|
|
78
|
+
# Install vendorized modules to save us from importing the same module twice
|
|
79
|
+
# if it already exists in the sys.modules
|
|
80
|
+
module = self._original_builtins_import_method(vendorized_import_path, globals, locals, fromlist, level)
|
|
81
|
+
imported_vendorized = True
|
|
82
|
+
except ModuleNotFoundError as exc:
|
|
83
|
+
LOGGER.debug(f"Failed to import vendorized: {name}: {exc!s}")
|
|
84
|
+
self._sys_modules_wrapper.remove_vendored_sys_modules()
|
|
85
|
+
module = self._original_builtins_import_method(name, globals, locals, fromlist, level)
|
|
86
|
+
imported_vendorized = False
|
|
87
|
+
|
|
88
|
+
if imported_vendorized:
|
|
89
|
+
is_absolute_import = fromlist is None or len(fromlist) == 0
|
|
90
|
+
if is_absolute_import:
|
|
91
|
+
module = self._retrieve_absolute_import_module(name, module)
|
|
92
|
+
self._add_imported_sub_modules_to_vendorized(name, module)
|
|
93
|
+
else:
|
|
94
|
+
# Add the non-vendored name to sys modules as well (its in the vendored
|
|
95
|
+
# sys modules so this should go to `package_modules` only)
|
|
96
|
+
sys.modules[name] = module
|
|
97
|
+
|
|
98
|
+
# It's important at the end to return to vendorized sys module if we imported
|
|
99
|
+
# a vendorized package in case an import was made inside this import which changed the state
|
|
100
|
+
self._sys_modules_wrapper.install_vendored_sys_modules()
|
|
101
|
+
|
|
102
|
+
LOGGER.debug(f"Imported: {module}")
|
|
103
|
+
return module
|
|
104
|
+
|
|
105
|
+
def importlib_import_override(
|
|
106
|
+
self,
|
|
107
|
+
name: str,
|
|
108
|
+
package: Optional[str] = None,
|
|
109
|
+
) -> ModuleType:
|
|
110
|
+
# If package is not None it's a relative import
|
|
111
|
+
if not self._should_import_vendorized(name) or package is not None:
|
|
112
|
+
self._sys_modules_wrapper.remove_vendored_sys_modules()
|
|
113
|
+
return self._original_importlib_import_method(name, package)
|
|
114
|
+
|
|
115
|
+
vendorized_import_path = self._vendor_import(name)
|
|
116
|
+
try:
|
|
117
|
+
self._sys_modules_wrapper.install_vendored_sys_modules()
|
|
118
|
+
return self._original_importlib_import_method(vendorized_import_path, None)
|
|
119
|
+
except ModuleNotFoundError as exc:
|
|
120
|
+
self._sys_modules_wrapper.remove_vendored_sys_modules()
|
|
121
|
+
LOGGER.debug(f"Failed to import vendorized: {name}: {exc!s}")
|
|
122
|
+
return self._original_importlib_import_method(name, None)
|
|
123
|
+
|
|
124
|
+
def _retrieve_absolute_import_module(
|
|
125
|
+
self,
|
|
126
|
+
module_name: str,
|
|
127
|
+
module_with_vendor_prefix: ModuleType,
|
|
128
|
+
) -> ModuleType:
|
|
129
|
+
module_without_vendor_prefix = self._try_retrieving_imported_module_by_getattr(
|
|
130
|
+
module_name=module_name,
|
|
131
|
+
returned_module=module_with_vendor_prefix,
|
|
132
|
+
)
|
|
133
|
+
if module_without_vendor_prefix is None:
|
|
134
|
+
self._sys_modules_wrapper.install_vendored_sys_modules()
|
|
135
|
+
module_without_vendor_prefix = self._retrieve_imported_module_from_sys_modules(
|
|
136
|
+
self._vendor_import(self._extract_returned_module_name(module_name))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return module_without_vendor_prefix
|
|
140
|
+
|
|
141
|
+
def _add_imported_sub_modules_to_vendorized(
|
|
142
|
+
self,
|
|
143
|
+
module_name: str,
|
|
144
|
+
returned_module: ModuleType,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Adds the imported sub-packages of the returned module to the vendorized modules state.
|
|
148
|
+
For example if we perform a vendorized: `import example_library.module` then we
|
|
149
|
+
return `example_library` but in the sys modules there will only be:
|
|
150
|
+
1. `example_project.libs.example_library`
|
|
151
|
+
2. `example_project.libs.example_library.module`
|
|
152
|
+
Meanwhile this function will also add the following:
|
|
153
|
+
1. `example_library`
|
|
154
|
+
2. `example_library.module`
|
|
155
|
+
"""
|
|
156
|
+
packages = module_name.split(".")
|
|
157
|
+
current_module = returned_module
|
|
158
|
+
for index, path_component in enumerate(packages):
|
|
159
|
+
if index > 0:
|
|
160
|
+
try:
|
|
161
|
+
current_module = getattr(current_module, path_component)
|
|
162
|
+
except AttributeError:
|
|
163
|
+
current_module = self._retrieve_imported_module_from_sys_modules(
|
|
164
|
+
self._vendor_import(".".join(packages[: index + 1]))
|
|
165
|
+
)
|
|
166
|
+
current_module_name = ".".join(packages[: index + 1])
|
|
167
|
+
sys.modules[current_module_name] = current_module
|
|
168
|
+
|
|
169
|
+
def _retrieve_imported_module_from_sys_modules(
|
|
170
|
+
self,
|
|
171
|
+
module_name: str,
|
|
172
|
+
) -> ModuleType:
|
|
173
|
+
module = sys.modules.get(module_name)
|
|
174
|
+
if module is None:
|
|
175
|
+
raise ModuleNotFoundError(f"No module named: '{module_name}'")
|
|
176
|
+
|
|
177
|
+
return module
|
|
178
|
+
|
|
179
|
+
def _try_retrieving_imported_module_by_getattr(
|
|
180
|
+
self,
|
|
181
|
+
module_name: str,
|
|
182
|
+
returned_module: ModuleType,
|
|
183
|
+
) -> Optional[ModuleType]:
|
|
184
|
+
"""
|
|
185
|
+
If we perform the following from example_project:
|
|
186
|
+
`import example_library.library_module`
|
|
187
|
+
This will reroute us to:
|
|
188
|
+
`import example_project.libs.example_library.library_module`
|
|
189
|
+
therefore instead of accessing the variable by using `example_library.library_module.MY_STRING`
|
|
190
|
+
we have to use `example_project.libs.example_library.library_module`.
|
|
191
|
+
To avoid this we have to return the `example_library` module instead of the `example_project` module.
|
|
192
|
+
|
|
193
|
+
This may fail sometimes due to circular imports not able to retrieve the object yet.
|
|
194
|
+
Ie: If you are already inside `example_library.__init__` and importing `example_library.library_module`
|
|
195
|
+
it might fail because this will try to access an attribute of a partially-initialized module.
|
|
196
|
+
"""
|
|
197
|
+
packages = self.vendor_prefix.split(".")[1:]
|
|
198
|
+
returned_module_name = self._extract_returned_module_name(module_name)
|
|
199
|
+
packages.append(returned_module_name)
|
|
200
|
+
|
|
201
|
+
return functools.reduce(
|
|
202
|
+
lambda module, package: getattr(module, package, None),
|
|
203
|
+
packages,
|
|
204
|
+
returned_module,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _vendor_import(self, import_path: str) -> str:
|
|
208
|
+
return f"{self.vendor_prefix}.{import_path}"
|
|
209
|
+
|
|
210
|
+
def _extract_returned_module_name(self, module_name: str) -> str:
|
|
211
|
+
returned_module_attribute, _, _ = module_name.partition(".")
|
|
212
|
+
return returned_module_attribute
|
|
213
|
+
|
|
214
|
+
def _should_import_vendorized(
|
|
215
|
+
self,
|
|
216
|
+
name: str,
|
|
217
|
+
) -> bool:
|
|
218
|
+
if name.startswith(RELATIVE_IMPORT_PREFIX):
|
|
219
|
+
LOGGER.debug("Relative imports don't require changing the import path for vendorized packages")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
if name.startswith(self.vendor_prefix):
|
|
223
|
+
LOGGER.debug(
|
|
224
|
+
"The attempted import is already for a vendorized package therefore there's "
|
|
225
|
+
"no need to change the import path"
|
|
226
|
+
)
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
if name == self.package_name:
|
|
230
|
+
LOGGER.debug(f"Cannot re-import the library: {self.package_name}")
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
if not is_caller_part_of_library(self.package_name):
|
|
234
|
+
LOGGER.debug("Cannot import because it's not part of library")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if is_part_of_stdlib(name):
|
|
238
|
+
LOGGER.debug(f"Skipping vendor import attempt for {name} because the same name exists in stdlib")
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
def find_distributions(
|
|
244
|
+
self,
|
|
245
|
+
context: Optional[DistributionFinder.Context] = None,
|
|
246
|
+
) -> Iterable[Distribution]:
|
|
247
|
+
if context is None:
|
|
248
|
+
context = DistributionFinder.Context()
|
|
249
|
+
|
|
250
|
+
LOGGER.debug(f"Finding distributions in VendorImporter for context: {context}")
|
|
251
|
+
if not is_caller_part_of_library(self.package_name):
|
|
252
|
+
LOGGER.debug("Returning empty list in find_distributions because not part of library")
|
|
253
|
+
return []
|
|
254
|
+
|
|
255
|
+
LOGGER.debug(f"Returning all distributions in vendorized path: {self.vendorized_libs_path}")
|
|
256
|
+
vars(context).update({"path": [str(self.vendorized_libs_path)]})
|
|
257
|
+
return MetadataPathFinder.find_distributions(context)
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def vendor_prefix(self) -> str:
|
|
261
|
+
return f"{self.package_name}.{self.vendorized_libs_relative_import_path}"
|
|
262
|
+
|
|
263
|
+
def install(self) -> None:
|
|
264
|
+
builtins.__import__ = self.builtins_import_override
|
|
265
|
+
importlib.import_module = self.importlib_import_override
|
|
266
|
+
# Distribution entrypoint loading uses the import_module in `importlib.metadata`
|
|
267
|
+
# and therefore needs to be overridden as well
|
|
268
|
+
importlib.metadata.import_module = self.importlib_import_override # pyright: ignore[reportAttributeAccessIssue]
|
|
269
|
+
|
|
270
|
+
if self not in sys.meta_path:
|
|
271
|
+
# For distribution finder
|
|
272
|
+
sys.meta_path.append(self)
|
|
273
|
+
|
|
274
|
+
sys.modules = self._sys_modules_wrapper
|
|
275
|
+
|
|
276
|
+
def uninstall(self) -> None:
|
|
277
|
+
builtins.__import__ = self._original_builtins_import_method
|
|
278
|
+
importlib.import_module = self._original_importlib_import_method
|
|
279
|
+
importlib.metadata.import_module = self._original_importlib_import_method # pyright: ignore[reportAttributeAccessIssue]
|
|
280
|
+
|
|
281
|
+
if self in sys.meta_path:
|
|
282
|
+
sys.meta_path.remove(self)
|
|
283
|
+
|
|
284
|
+
sys.modules = self._sys_modules_wrapper.original_sys_modules
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_installed_vendor_importer() -> Optional[VendorImporter]:
|
|
288
|
+
import_owner = getattr(builtins.__import__, "__self__", None)
|
|
289
|
+
if (import_owner is None) or (not isinstance(import_owner, VendorImporter)):
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
return import_owner
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from collections import UserDict
|
|
2
|
+
from collections.abc import MutableMapping
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from reyk.caller_finder import is_caller_part_of_library
|
|
6
|
+
from reyk.stdlib_finder import is_part_of_stdlib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SysModulesSnapshot:
|
|
11
|
+
original_sys_modules: dict[str, ModuleType]
|
|
12
|
+
user_modules: dict[str, ModuleType]
|
|
13
|
+
package_modules: dict[str, ModuleType]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VendoredSysModules(UserDict[str, ModuleType]):
|
|
17
|
+
"""
|
|
18
|
+
Wrapper for overriding python-direct access to return vendored modules based on
|
|
19
|
+
stack trace context. This will usually not effect the actual import mechanism as it access
|
|
20
|
+
the original `sys.modules`.
|
|
21
|
+
|
|
22
|
+
Therefore we are doing best-effort here:
|
|
23
|
+
1. If there's access from C/the original sys modules then we ensure to take the vendored module only if we're in
|
|
24
|
+
in a vendored context (in the middle of a vendored import), otherwise we'll use the
|
|
25
|
+
original user imported module/no module with the same name
|
|
26
|
+
2. If we access from Python to the sys modules wrapper then we'll never access the original
|
|
27
|
+
sys modules and therefore use the appropriate modules based on the vendor context (user module if outside vendor
|
|
28
|
+
and vendor module if inside vendor)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
DICT_INTERNAL_DATA_FIELD_NAME = "data"
|
|
32
|
+
|
|
33
|
+
def __init__(self, original_sys_modules: dict[str, ModuleType], package_name: str, vendor_prefix: str) -> None:
|
|
34
|
+
self._package_name = package_name
|
|
35
|
+
self._vendor_prefix = vendor_prefix
|
|
36
|
+
self.original_sys_modules = original_sys_modules
|
|
37
|
+
self._package_modules: dict[str, ModuleType] = original_sys_modules.copy()
|
|
38
|
+
self._user_modules: dict[str, ModuleType] = original_sys_modules.copy()
|
|
39
|
+
self._are_vendored_modules_installed = False
|
|
40
|
+
|
|
41
|
+
def install_vendored_sys_modules(self) -> None:
|
|
42
|
+
if self._are_vendored_modules_installed:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
self._switch_original_sys_modules_state(self._package_modules, self._user_modules)
|
|
46
|
+
self._are_vendored_modules_installed = True
|
|
47
|
+
|
|
48
|
+
def remove_vendored_sys_modules(self) -> None:
|
|
49
|
+
if not self._are_vendored_modules_installed:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
self._switch_original_sys_modules_state(self._user_modules, self._package_modules)
|
|
53
|
+
self._are_vendored_modules_installed = False
|
|
54
|
+
|
|
55
|
+
def _switch_original_sys_modules_state(
|
|
56
|
+
self,
|
|
57
|
+
new_sys_modules: dict[str, ModuleType],
|
|
58
|
+
previous_sys_modules: dict[str, ModuleType],
|
|
59
|
+
) -> None:
|
|
60
|
+
for module_name in previous_sys_modules.keys():
|
|
61
|
+
self.original_sys_modules.pop(module_name, None)
|
|
62
|
+
|
|
63
|
+
self.original_sys_modules.update(new_sys_modules)
|
|
64
|
+
|
|
65
|
+
def take_snapshot(self) -> SysModulesSnapshot:
|
|
66
|
+
return SysModulesSnapshot(
|
|
67
|
+
original_sys_modules=self.original_sys_modules.copy(),
|
|
68
|
+
user_modules=self._user_modules.copy(),
|
|
69
|
+
package_modules=self._package_modules.copy(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def restore_snapshot(self, snapshot: SysModulesSnapshot) -> None:
|
|
73
|
+
for current_modules, snapshot_modules in (
|
|
74
|
+
(self.original_sys_modules, snapshot.original_sys_modules),
|
|
75
|
+
(self._user_modules, snapshot.user_modules),
|
|
76
|
+
(self._package_modules, snapshot.package_modules),
|
|
77
|
+
):
|
|
78
|
+
current_modules.clear()
|
|
79
|
+
current_modules.update(snapshot_modules)
|
|
80
|
+
|
|
81
|
+
def __setitem__(self, key: str, value: ModuleType) -> None:
|
|
82
|
+
module_name = value.__name__
|
|
83
|
+
if is_part_of_stdlib(module_name) or module_name == self._package_name:
|
|
84
|
+
# Standard libraries should be registered as both package & user modules
|
|
85
|
+
self.original_sys_modules[key] = value
|
|
86
|
+
self._package_modules[key] = value
|
|
87
|
+
self._user_modules[key] = value
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
dicts_to_update: list[MutableMapping[str, ModuleType]] = [super()]
|
|
91
|
+
if is_caller_part_of_library(self._package_name) == self._are_vendored_modules_installed:
|
|
92
|
+
dicts_to_update.append(self.original_sys_modules)
|
|
93
|
+
|
|
94
|
+
# The setitem will be directed to package modules/user modules
|
|
95
|
+
for dict_to_update in dicts_to_update:
|
|
96
|
+
if key.startswith(self._vendor_prefix) and key != self._vendor_prefix:
|
|
97
|
+
dict_to_update.__setitem__(key.removeprefix(self._vendor_prefix).removeprefix("."), value)
|
|
98
|
+
|
|
99
|
+
dict_to_update.__setitem__(key, value)
|
|
100
|
+
|
|
101
|
+
def __getattribute__(self, name: str) -> object:
|
|
102
|
+
if name == VendoredSysModules.DICT_INTERNAL_DATA_FIELD_NAME:
|
|
103
|
+
if is_caller_part_of_library(self._package_name):
|
|
104
|
+
return self._package_modules
|
|
105
|
+
|
|
106
|
+
return self._user_modules
|
|
107
|
+
|
|
108
|
+
return super().__getattribute__(name)
|
reyk/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.0.1"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reyk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Run conflicting Python dependencies in one process — with vendored isolation.
|
|
5
|
+
Project-URL: Homepage, https://reyk.dev
|
|
6
|
+
Project-URL: Source, https://github.com/eyalohn/reyk
|
|
7
|
+
Author-email: Eyal Ohn <eyal.ohn@gmail.com>, Ofek Ashery <ofek.ashery@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: conflicting dependencies,dependency isolation,python import system,vendoring,virtual environment
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Reyk
|
|
27
|
+
|
|
28
|
+
[](https://github.com/eyalohn/reyk/actions/workflows/test.yml)
|
|
29
|
+
[](https://coverage-badge.samuelcolvin.workers.dev/redirect/eyalohn/reyk)
|
|
30
|
+
[](https://pypi.python.org/pypi/reyk/)
|
|
31
|
+
[](https://pypi.python.org/pypi/reyk/)
|
|
32
|
+
[](https://reyk.dev/llms-full.txt)
|
|
33
|
+
|
|
34
|
+
Run conflicting Python dependencies in one process — with vendored isolation.
|
|
35
|
+
|
|
36
|
+
[Reyk](https://reyk.dev) lets you **vendor Python dependencies into your project and isolate them by design**, without changing how you write imports. We leverage Python's import system internals to achieve transparent dependency isolation.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Zero Configuration**: Enable isolation with a single function call
|
|
41
|
+
- **Dependency Isolation**: Avoid conflicts by shipping libraries with your package
|
|
42
|
+
- **Version Flexibility**: Run multiple versions of the same library in one process
|
|
43
|
+
- **Transparent Imports**: Keep your public imports unchanged (`import requests` still works)
|
|
44
|
+
- **Lightweight**: Minimal overhead with a tiny import hook
|
|
45
|
+
|
|
46
|
+
# Installation
|
|
47
|
+
|
|
48
|
+
You can install Reyk from PyPI:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv add reyk
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Enable isolation from your package's `__init__.py`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from reyk.isolator import isolate_package
|
|
58
|
+
|
|
59
|
+
isolate_package()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then, vendor your dependencies with Reyk's CLI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uvx reyk-cli add requests==2.25.1
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**That's it!** Inside `your_package`, `import requests` resolves to `your_package.libs.requests`. Outside `your_package`, `import requests` continues to resolve to the global installation.
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
For detailed documentation, examples, and API reference, visit the documentation site at https://reyk.dev.
|
|
73
|
+
|
|
74
|
+
## Limitations
|
|
75
|
+
|
|
76
|
+
- Multi-threaded applications are not supported.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
reyk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
reyk/caller_finder.py,sha256=yxf72ExThw4HsfGm3HUvINmdoxjP3jTiN0KZ3DOFN-A,1472
|
|
3
|
+
reyk/isolator.py,sha256=U1jFAfNgfZmYx8IYCA3rvzL37mZGtTGqWG4eEV0Hmx8,1978
|
|
4
|
+
reyk/stdlib_finder.py,sha256=_zTWjLi8CBfDKChKwwYi82eVZ5ns5gOjCe22e4iUSeg,758
|
|
5
|
+
reyk/vendor_importer.py,sha256=TQL93n9yjNgwsFAyM7XK_37BWH7hJQr3fBn-7gihG5A,12215
|
|
6
|
+
reyk/vendored_sys_modules.py,sha256=A5gUM1V8mLYK7XLxDXo07wi5hhSUwhztX4XgiCWJEZA,4710
|
|
7
|
+
reyk/version.py,sha256=Q8raD6k02z4DoYniAz3iui5d6EdM1el95SMTLXJbeCA,18
|
|
8
|
+
reyk-0.0.1.dist-info/METADATA,sha256=CnuCWSWmRpxIKlUMFs8Fmq8iSK_7ZbbsbJnBKOHCLNA,3134
|
|
9
|
+
reyk-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
reyk-0.0.1.dist-info/licenses/LICENSE,sha256=jS6aMH0eoPNwDHyW2R8su67bEUx4D0FKGQsu6-JPtGM,1071
|
|
11
|
+
reyk-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reyk's authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|