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 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()
@@ -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
+ [![Tests](https://github.com/eyalohn/reyk/actions/workflows/test.yml/badge.svg)](https://github.com/eyalohn/reyk/actions/workflows/test.yml)
29
+ [![Coverage](https://coverage-badge.samuelcolvin.workers.dev/eyalohn/reyk.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/eyalohn/reyk)
30
+ [![Latest Version](https://img.shields.io/pypi/v/reyk.svg)](https://pypi.python.org/pypi/reyk/)
31
+ [![Python Versions](https://img.shields.io/pypi/pyversions/reyk.svg)](https://pypi.python.org/pypi/reyk/)
32
+ [![llms.txt](https://img.shields.io/badge/llms.txt-green)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.