exonware-xwlazy 0.1.0.22__py3-none-any.whl → 0.1.0.23__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.
- exonware/__init__.py +20 -1
- exonware/xwlazy/__init__.py +14 -2
- exonware/xwlazy/common/__init__.py +8 -0
- exonware/xwlazy/common/base.py +11 -2
- exonware/xwlazy/common/cache.py +5 -5
- exonware/xwlazy/common/logger.py +5 -5
- exonware/xwlazy/common/services/dependency_mapper.py +31 -13
- exonware/xwlazy/common/services/install_async_utils.py +5 -0
- exonware/xwlazy/common/services/install_cache_utils.py +4 -4
- exonware/xwlazy/common/services/spec_cache.py +2 -2
- exonware/xwlazy/common/services/state_manager.py +4 -4
- exonware/xwlazy/common/strategies/caching_dict.py +2 -2
- exonware/xwlazy/common/strategies/caching_lfu.py +2 -2
- exonware/xwlazy/common/strategies/caching_ttl.py +2 -2
- exonware/xwlazy/common/utils.py +142 -0
- exonware/xwlazy/config.py +1 -1
- exonware/xwlazy/contracts.py +162 -25
- exonware/xwlazy/defs.py +15 -15
- exonware/xwlazy/facade.py +175 -29
- exonware/xwlazy/host/__init__.py +8 -0
- exonware/xwlazy/host/conf.py +16 -0
- exonware/xwlazy/module/base.py +61 -4
- exonware/xwlazy/module/facade.py +1 -1
- exonware/xwlazy/module/importer_engine.py +1017 -170
- exonware/xwlazy/module/partial_module_detector.py +275 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +3 -3
- exonware/xwlazy/package/base.py +106 -41
- exonware/xwlazy/package/conf.py +6 -6
- exonware/xwlazy/package/services/config_manager.py +20 -16
- exonware/xwlazy/package/services/discovery.py +81 -16
- exonware/xwlazy/package/services/host_packages.py +41 -6
- exonware/xwlazy/package/services/install_async.py +16 -2
- exonware/xwlazy/package/services/install_cache.py +4 -4
- exonware/xwlazy/package/services/install_policy.py +14 -14
- exonware/xwlazy/package/services/install_registry.py +3 -3
- exonware/xwlazy/package/services/install_sbom.py +1 -1
- exonware/xwlazy/package/services/installer_engine.py +3 -3
- exonware/xwlazy/package/services/lazy_installer.py +102 -17
- exonware/xwlazy/package/services/manifest.py +43 -36
- exonware/xwlazy/package/services/strategy_registry.py +150 -12
- exonware/xwlazy/package/strategies/package_discovery_file.py +2 -2
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +2 -2
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_async.py +3 -3
- exonware/xwlazy/package/strategies/package_execution_cached.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_pip.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_wheel.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +2 -2
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +4 -4
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +4 -4
- exonware/xwlazy/package/strategies/package_policy_permissive.py +3 -3
- exonware/xwlazy/package/strategies/package_timing_clean.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_full.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_smart.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_temporary.py +2 -2
- exonware/xwlazy/runtime/adaptive_learner.py +7 -7
- exonware/xwlazy/runtime/base.py +14 -14
- exonware/xwlazy/runtime/facade.py +7 -7
- exonware/xwlazy/runtime/intelligent_selector.py +6 -6
- exonware/xwlazy/runtime/metrics.py +6 -6
- exonware/xwlazy/runtime/performance.py +5 -5
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/METADATA +2 -6
- exonware_xwlazy-0.1.0.23.dist-info/RECORD +93 -0
- xwlazy/__init__.py +14 -0
- xwlazy/lazy.py +30 -0
- exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Partial Module Detection Strategies
|
|
3
|
+
|
|
4
|
+
Detects if a module in sys.modules is partially initialized (still being imported).
|
|
5
|
+
This prevents returning partially initialized modules that cause ImportErrors.
|
|
6
|
+
|
|
7
|
+
Company: eXonware.com
|
|
8
|
+
Author: Eng. Muhammad AlShehri
|
|
9
|
+
Email: connect@exonware.com
|
|
10
|
+
|
|
11
|
+
Generation Date: 27-Dec-2025
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
from typing import Optional
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
class DetectionStrategy(Enum):
|
|
21
|
+
"""Different strategies for detecting partially initialized modules."""
|
|
22
|
+
FRAME_STACK = "frame_stack" # Check call stack for import functions
|
|
23
|
+
ATTRIBUTE_CHECK = "attribute_check" # Check if expected attributes exist
|
|
24
|
+
IMPORT_LOCK = "import_lock" # Use importlib's import lock state
|
|
25
|
+
MODULE_STATE = "module_state" # Check module's __spec__ and loader state
|
|
26
|
+
HYBRID = "hybrid" # Combine multiple strategies
|
|
27
|
+
|
|
28
|
+
# Track modules currently being imported (thread-safe)
|
|
29
|
+
_importing_modules: set[str] = set()
|
|
30
|
+
_import_lock = threading.RLock()
|
|
31
|
+
|
|
32
|
+
def mark_module_importing(module_name: str) -> None:
|
|
33
|
+
"""Mark a module as currently being imported."""
|
|
34
|
+
with _import_lock:
|
|
35
|
+
_importing_modules.add(module_name)
|
|
36
|
+
|
|
37
|
+
def unmark_module_importing(module_name: str) -> None:
|
|
38
|
+
"""Unmark a module as no longer being imported."""
|
|
39
|
+
with _import_lock:
|
|
40
|
+
_importing_modules.discard(module_name)
|
|
41
|
+
|
|
42
|
+
def is_module_importing(module_name: str) -> bool:
|
|
43
|
+
"""Check if a module is currently being imported."""
|
|
44
|
+
with _import_lock:
|
|
45
|
+
return module_name in _importing_modules
|
|
46
|
+
|
|
47
|
+
class PartialModuleDetector:
|
|
48
|
+
"""
|
|
49
|
+
Modular detector for partially initialized modules.
|
|
50
|
+
|
|
51
|
+
Uses different strategies to detect if a module in sys.modules
|
|
52
|
+
is still being initialized and shouldn't be returned yet.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, strategy: DetectionStrategy = DetectionStrategy.HYBRID):
|
|
56
|
+
self.strategy = strategy
|
|
57
|
+
|
|
58
|
+
def is_partially_initialized(self, module_name: str, module: ModuleType) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if module is partially initialized (still being imported).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
module_name: Name of the module
|
|
64
|
+
module: Module object from sys.modules
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if module is partially initialized, False if fully loaded
|
|
68
|
+
"""
|
|
69
|
+
if self.strategy == DetectionStrategy.FRAME_STACK:
|
|
70
|
+
return self._check_frame_stack(module_name)
|
|
71
|
+
elif self.strategy == DetectionStrategy.ATTRIBUTE_CHECK:
|
|
72
|
+
return self._check_attributes(module_name, module)
|
|
73
|
+
elif self.strategy == DetectionStrategy.IMPORT_LOCK:
|
|
74
|
+
return self._check_import_lock(module_name)
|
|
75
|
+
elif self.strategy == DetectionStrategy.MODULE_STATE:
|
|
76
|
+
return self._check_module_state(module_name, module)
|
|
77
|
+
elif self.strategy == DetectionStrategy.HYBRID:
|
|
78
|
+
return self._check_hybrid(module_name, module)
|
|
79
|
+
else:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def _check_frame_stack(self, module_name: str) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Strategy 1: Check call stack for import-related functions.
|
|
85
|
+
|
|
86
|
+
If we're in _find_and_load, _load_unlocked, or exec_module,
|
|
87
|
+
the module is likely still being imported.
|
|
88
|
+
|
|
89
|
+
CRITICAL: This checks if the CURRENT import call is for this module,
|
|
90
|
+
not just if we're in an import function.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# Start from frame 2 (skip our own function and the caller)
|
|
94
|
+
frame = sys._getframe(2)
|
|
95
|
+
depth = 0
|
|
96
|
+
max_depth = 30 # Increased depth to catch deeper import chains
|
|
97
|
+
|
|
98
|
+
while frame and depth < max_depth:
|
|
99
|
+
code = frame.f_code
|
|
100
|
+
func_name = code.co_name
|
|
101
|
+
|
|
102
|
+
# Check for import-related functions in Python's importlib
|
|
103
|
+
import_keywords = [
|
|
104
|
+
'_find_and_load', '_load_unlocked', 'exec_module',
|
|
105
|
+
'_call_with_frames_removed', '_load_module_spec',
|
|
106
|
+
'_intercepting_import', '__import__'
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
if any(keyword in func_name for keyword in import_keywords):
|
|
110
|
+
# Check if this module is in the frame's locals or globals
|
|
111
|
+
if hasattr(frame, 'f_locals'):
|
|
112
|
+
locals_dict = frame.f_locals
|
|
113
|
+
# Check various ways module name might appear
|
|
114
|
+
frame_module_name = (
|
|
115
|
+
locals_dict.get('name') or
|
|
116
|
+
locals_dict.get('fullname') or
|
|
117
|
+
locals_dict.get('__name__') or
|
|
118
|
+
(locals_dict.get('module') and getattr(locals_dict.get('module'), '__name__', None))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if frame_module_name == module_name:
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
# Also check globals
|
|
125
|
+
if hasattr(frame, 'f_globals'):
|
|
126
|
+
globals_dict = frame.f_globals
|
|
127
|
+
if '__name__' in globals_dict and globals_dict.get('__name__') == module_name:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
frame = frame.f_back
|
|
131
|
+
depth += 1
|
|
132
|
+
except (AttributeError, ValueError):
|
|
133
|
+
# Frame inspection failed, assume not importing
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def _check_attributes(self, module_name: str, module: ModuleType) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Strategy 2: Check if module has expected attributes.
|
|
141
|
+
|
|
142
|
+
A fully initialized module should have certain attributes.
|
|
143
|
+
If key attributes are missing, it might be partially initialized.
|
|
144
|
+
|
|
145
|
+
CRITICAL: We need to be careful - some modules legitimately don't have
|
|
146
|
+
__file__ (namespace packages, built-ins). We only flag as partial if
|
|
147
|
+
we're CERTAIN it's not fully initialized.
|
|
148
|
+
"""
|
|
149
|
+
module_dict = getattr(module, '__dict__', {})
|
|
150
|
+
|
|
151
|
+
# Check for placeholder patterns (lazy loaders often add __getattr__)
|
|
152
|
+
if hasattr(module, '__getattr__'):
|
|
153
|
+
# If it has __getattr__ but very few attributes, it's likely a placeholder
|
|
154
|
+
if len(module_dict) <= 5: # Only __name__, __loader__, __spec__, __getattr__, etc.
|
|
155
|
+
if not hasattr(module, '__file__') and not hasattr(module, '__path__'):
|
|
156
|
+
# Not a namespace package and no file - likely placeholder
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
# Check if module has __spec__ but loader hasn't executed yet
|
|
160
|
+
if hasattr(module, '__spec__') and module.__spec__ is not None:
|
|
161
|
+
spec = module.__spec__
|
|
162
|
+
# If loader exists but module dict is very small, it might not be fully loaded
|
|
163
|
+
if hasattr(spec, 'loader') and spec.loader is not None:
|
|
164
|
+
# Check if module dict is suspiciously small (only metadata, no actual content)
|
|
165
|
+
if len(module_dict) <= 3:
|
|
166
|
+
# Only has __name__, __loader__, __spec__ - likely not executed
|
|
167
|
+
if not hasattr(module, '__file__') and not hasattr(module, '__path__'):
|
|
168
|
+
# Not a namespace package - likely partial
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
# If module is in sys.modules but has no meaningful content, it's likely partial
|
|
172
|
+
# This is a heuristic - be conservative
|
|
173
|
+
if module_name in sys.modules:
|
|
174
|
+
# Check if module has been executed by looking for non-metadata attributes
|
|
175
|
+
metadata_attrs = {'__name__', '__loader__', '__spec__', '__package__', '__file__', '__path__', '__cached__'}
|
|
176
|
+
content_attrs = set(module_dict.keys()) - metadata_attrs
|
|
177
|
+
if len(content_attrs) == 0 and len(module_dict) > 0:
|
|
178
|
+
# Has metadata but no content - might be partial
|
|
179
|
+
# But be conservative - only flag if we're sure
|
|
180
|
+
if hasattr(module, '__loader__') and module.__loader__ is not None:
|
|
181
|
+
# Has loader but no content - likely partial
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def _check_import_lock(self, module_name: str) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Strategy 3: Use our own import tracking.
|
|
189
|
+
|
|
190
|
+
Check if module is marked as currently being imported.
|
|
191
|
+
"""
|
|
192
|
+
return is_module_importing(module_name)
|
|
193
|
+
|
|
194
|
+
def _check_module_state(self, module_name: str, module: ModuleType) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
Strategy 4: Check module's internal state.
|
|
197
|
+
|
|
198
|
+
Look at __spec__, __loader__, and other state indicators.
|
|
199
|
+
"""
|
|
200
|
+
# If module doesn't have __spec__, it's likely not fully initialized
|
|
201
|
+
if not hasattr(module, '__spec__') or module.__spec__ is None:
|
|
202
|
+
# Exception: Some built-in modules don't have __spec__
|
|
203
|
+
if module_name not in sys.builtin_module_names:
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
# Check if module has __loader__ but loader hasn't executed
|
|
207
|
+
if hasattr(module, '__loader__') and module.__loader__ is not None:
|
|
208
|
+
loader = module.__loader__
|
|
209
|
+
# If loader has exec_module but module doesn't have expected attributes,
|
|
210
|
+
# it might not be fully executed
|
|
211
|
+
if hasattr(loader, 'exec_module'):
|
|
212
|
+
# Check if module has been executed by looking for common attributes
|
|
213
|
+
# This is heuristic - modules should have some content after execution
|
|
214
|
+
module_dict = getattr(module, '__dict__', {})
|
|
215
|
+
# If dict is mostly empty (only has __name__, __loader__, __spec__),
|
|
216
|
+
# it might not be fully initialized
|
|
217
|
+
if len(module_dict) <= 3:
|
|
218
|
+
# Check if it's a legitimate empty module
|
|
219
|
+
if '__file__' not in module_dict and '__path__' not in module_dict:
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
def _check_hybrid(self, module_name: str, module: ModuleType) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Strategy 5: Hybrid - combine multiple strategies.
|
|
227
|
+
|
|
228
|
+
Returns True if ANY strategy indicates partial initialization.
|
|
229
|
+
This is the most conservative approach.
|
|
230
|
+
|
|
231
|
+
NOTE: We check frame stack FIRST because it's most accurate for detecting
|
|
232
|
+
modules currently being imported. Import lock is secondary.
|
|
233
|
+
"""
|
|
234
|
+
# Check frame stack first (most accurate for detecting active imports)
|
|
235
|
+
if self._check_frame_stack(module_name):
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
# Check attributes (fast, reliable)
|
|
239
|
+
if self._check_attributes(module_name, module):
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
# Check module state (medium speed)
|
|
243
|
+
if self._check_module_state(module_name, module):
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
# Check import lock last (may have false positives)
|
|
247
|
+
# Only use if other checks are inconclusive
|
|
248
|
+
if is_module_importing(module_name):
|
|
249
|
+
# Double-check with frame stack to avoid false positives
|
|
250
|
+
if self._check_frame_stack(module_name):
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# Global detector instance (defaults to HYBRID strategy)
|
|
256
|
+
_default_detector = PartialModuleDetector(DetectionStrategy.HYBRID)
|
|
257
|
+
|
|
258
|
+
def is_partially_initialized(module_name: str, module: ModuleType,
|
|
259
|
+
strategy: Optional[DetectionStrategy] = None) -> bool:
|
|
260
|
+
"""
|
|
261
|
+
Convenience function to check if module is partially initialized.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
module_name: Name of the module
|
|
265
|
+
module: Module object from sys.modules
|
|
266
|
+
strategy: Optional strategy override
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
True if module is partially initialized
|
|
270
|
+
"""
|
|
271
|
+
if strategy:
|
|
272
|
+
detector = PartialModuleDetector(strategy)
|
|
273
|
+
return detector.is_partially_initialized(module_name, module)
|
|
274
|
+
return _default_detector.is_partially_initialized(module_name, module)
|
|
275
|
+
|
|
@@ -14,7 +14,7 @@ import importlib
|
|
|
14
14
|
import importlib.util
|
|
15
15
|
import threading
|
|
16
16
|
from types import ModuleType
|
|
17
|
-
from typing import Any, Optional
|
|
17
|
+
from typing import Any, Optional
|
|
18
18
|
from ...module.base import AModuleHelperStrategy
|
|
19
19
|
from ..data import ModuleData
|
|
20
20
|
|
|
@@ -28,8 +28,8 @@ class LazyHelper(AModuleHelperStrategy):
|
|
|
28
28
|
|
|
29
29
|
def __init__(self):
|
|
30
30
|
"""Initialize lazy helper."""
|
|
31
|
-
self._cache:
|
|
32
|
-
self._loading:
|
|
31
|
+
self._cache: dict[str, ModuleType] = {}
|
|
32
|
+
self._loading: dict[str, bool] = {}
|
|
33
33
|
self._lock = threading.RLock()
|
|
34
34
|
|
|
35
35
|
def load(self, module_path: str, package_helper: Any) -> ModuleType:
|
exonware/xwlazy/package/base.py
CHANGED
|
@@ -15,7 +15,7 @@ This module defines the abstract base class for package operations.
|
|
|
15
15
|
import threading
|
|
16
16
|
from abc import ABC, abstractmethod
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import
|
|
18
|
+
from typing import Optional, Any
|
|
19
19
|
from types import ModuleType
|
|
20
20
|
|
|
21
21
|
from ..defs import (
|
|
@@ -31,6 +31,7 @@ from ..contracts import (
|
|
|
31
31
|
IDiscoveryStrategy,
|
|
32
32
|
IPolicyStrategy,
|
|
33
33
|
IMappingStrategy,
|
|
34
|
+
IInstallStrategy,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
# =============================================================================
|
|
@@ -78,24 +79,24 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
78
79
|
"""
|
|
79
80
|
# From APackageDiscovery
|
|
80
81
|
self.project_root = Path(project_root) if project_root else self._find_project_root()
|
|
81
|
-
self.discovered_dependencies:
|
|
82
|
-
self._discovery_sources:
|
|
83
|
-
self._cached_dependencies:
|
|
84
|
-
self._file_mtimes:
|
|
82
|
+
self.discovered_dependencies: dict[str, DependencyInfo] = {}
|
|
83
|
+
self._discovery_sources: list[str] = []
|
|
84
|
+
self._cached_dependencies: dict[str, str] = {}
|
|
85
|
+
self._file_mtimes: dict[str, float] = {}
|
|
85
86
|
self._cache_valid = False
|
|
86
87
|
|
|
87
88
|
# From APackageInstaller
|
|
88
89
|
self._package_name = package_name
|
|
89
90
|
self._enabled = False
|
|
90
91
|
self._mode = LazyInstallMode.SMART
|
|
91
|
-
self._installed_packages:
|
|
92
|
-
self._failed_packages:
|
|
92
|
+
self._installed_packages: set[str] = set()
|
|
93
|
+
self._failed_packages: set[str] = set()
|
|
93
94
|
|
|
94
95
|
# From APackageCache
|
|
95
|
-
self._cache:
|
|
96
|
+
self._cache: dict[str, Any] = {}
|
|
96
97
|
|
|
97
98
|
# From APackageHelper
|
|
98
|
-
self._uninstalled_packages:
|
|
99
|
+
self._uninstalled_packages: set[str] = set()
|
|
99
100
|
|
|
100
101
|
# Common
|
|
101
102
|
self._lock = threading.RLock()
|
|
@@ -105,15 +106,15 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
105
106
|
# ========================================================================
|
|
106
107
|
|
|
107
108
|
def _find_project_root(self) -> Path:
|
|
108
|
-
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return
|
|
109
|
+
"""
|
|
110
|
+
Find the project root directory by looking for markers.
|
|
111
|
+
|
|
112
|
+
Uses the shared utility function from common.utils.
|
|
113
|
+
"""
|
|
114
|
+
from ..common.utils import find_project_root
|
|
115
|
+
return find_project_root()
|
|
115
116
|
|
|
116
|
-
def discover_all_dependencies(self) ->
|
|
117
|
+
def discover_all_dependencies(self) -> dict[str, str]:
|
|
117
118
|
"""
|
|
118
119
|
Template method: Discover all dependencies from all sources.
|
|
119
120
|
|
|
@@ -186,7 +187,7 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
186
187
|
"""Update file modification times for cache validation (abstract step)."""
|
|
187
188
|
pass
|
|
188
189
|
|
|
189
|
-
def get_discovery_sources(self) ->
|
|
190
|
+
def get_discovery_sources(self) -> list[str]:
|
|
190
191
|
"""Get list of sources used for discovery."""
|
|
191
192
|
return self._discovery_sources.copy()
|
|
192
193
|
|
|
@@ -194,9 +195,17 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
194
195
|
# Package Installation Methods (from APackageInstaller)
|
|
195
196
|
# ========================================================================
|
|
196
197
|
|
|
197
|
-
def get_package_name(self) -> str:
|
|
198
|
-
"""
|
|
199
|
-
|
|
198
|
+
def get_package_name(self, import_name: Optional[str] = None) -> Optional[str]:
|
|
199
|
+
"""
|
|
200
|
+
Get package name.
|
|
201
|
+
If import_name is None, returns the package name this instance is for.
|
|
202
|
+
If import_name is provided, maps it to a package name (via IDependencyMapper).
|
|
203
|
+
"""
|
|
204
|
+
if import_name is None:
|
|
205
|
+
return self._package_name
|
|
206
|
+
|
|
207
|
+
# For IDependencyMapper implementation
|
|
208
|
+
raise NotImplementedError("Subclasses must implement get_package_name(import_name)")
|
|
200
209
|
|
|
201
210
|
def set_mode(self, mode: LazyInstallMode) -> None:
|
|
202
211
|
"""Set the installation mode."""
|
|
@@ -236,7 +245,7 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
236
245
|
pass
|
|
237
246
|
|
|
238
247
|
@abstractmethod
|
|
239
|
-
def _check_security_policy(self, package_name: str) ->
|
|
248
|
+
def _check_security_policy(self, package_name: str) -> tuple[bool, str]:
|
|
240
249
|
"""
|
|
241
250
|
Check security policy for package (abstract method).
|
|
242
251
|
|
|
@@ -249,7 +258,7 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
249
258
|
pass
|
|
250
259
|
|
|
251
260
|
@abstractmethod
|
|
252
|
-
def _run_pip_install(self, package_name: str, args:
|
|
261
|
+
def _run_pip_install(self, package_name: str, args: list[str]) -> bool:
|
|
253
262
|
"""
|
|
254
263
|
Run pip install with arguments (abstract method).
|
|
255
264
|
|
|
@@ -262,7 +271,7 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
262
271
|
"""
|
|
263
272
|
pass
|
|
264
273
|
|
|
265
|
-
def get_stats(self) ->
|
|
274
|
+
def get_stats(self) -> dict[str, Any]:
|
|
266
275
|
"""Get installation statistics."""
|
|
267
276
|
with self._lock:
|
|
268
277
|
return {
|
|
@@ -548,7 +557,7 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
548
557
|
# Note: Many methods from IPackageHelper are already implemented above.
|
|
549
558
|
# The following are stubs that need concrete implementations:
|
|
550
559
|
|
|
551
|
-
def install_and_import(self, module_name: str, package_name: Optional[str] = None) ->
|
|
560
|
+
def install_and_import(self, module_name: str, package_name: Optional[str] = None) -> tuple[Optional[ModuleType], bool]:
|
|
552
561
|
"""Install package and import module (from IPackageInstaller)."""
|
|
553
562
|
raise NotImplementedError("Subclasses must implement install_and_import")
|
|
554
563
|
|
|
@@ -556,15 +565,15 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
556
565
|
"""Get package name for a given import name (from IPackageDiscovery)."""
|
|
557
566
|
raise NotImplementedError("Subclasses must implement get_package_for_import")
|
|
558
567
|
|
|
559
|
-
def get_imports_for_package(self, package_name: str) ->
|
|
568
|
+
def get_imports_for_package(self, package_name: str) -> list[str]:
|
|
560
569
|
"""Get all possible import names for a package (from IPackageDiscovery)."""
|
|
561
570
|
raise NotImplementedError("Subclasses must implement get_imports_for_package")
|
|
562
571
|
|
|
563
|
-
def get_package_name(self, import_name: str) -> Optional[str]:
|
|
564
|
-
|
|
565
|
-
|
|
572
|
+
# def get_package_name(self, import_name: str) -> Optional[str]:
|
|
573
|
+
# """Get package name for an import name (from IDependencyMapper)."""
|
|
574
|
+
# raise NotImplementedError("Subclasses must implement get_package_name")
|
|
566
575
|
|
|
567
|
-
def get_import_names(self, package_name: str) ->
|
|
576
|
+
def get_import_names(self, package_name: str) -> list[str]:
|
|
568
577
|
"""Get all import names for a package (from IDependencyMapper)."""
|
|
569
578
|
raise NotImplementedError("Subclasses must implement get_import_names")
|
|
570
579
|
|
|
@@ -591,15 +600,15 @@ class APackageHelper(IPackageHelper, ABC):
|
|
|
591
600
|
"""Get full mode configuration for a package (from IConfigManager)."""
|
|
592
601
|
raise NotImplementedError("Subclasses must implement get_mode_config")
|
|
593
602
|
|
|
594
|
-
def get_manifest_signature(self, package_name: str) -> Optional[
|
|
603
|
+
def get_manifest_signature(self, package_name: str) -> Optional[tuple[str, float, float]]:
|
|
595
604
|
"""Get manifest file signature (from IManifestLoader)."""
|
|
596
605
|
raise NotImplementedError("Subclasses must implement get_manifest_signature")
|
|
597
606
|
|
|
598
|
-
def get_shared_dependencies(self, package_name: str, signature: Optional[
|
|
607
|
+
def get_shared_dependencies(self, package_name: str, signature: Optional[tuple[str, float, float]] = None) -> dict[str, str]:
|
|
599
608
|
"""Get shared dependencies from manifest (from IManifestLoader)."""
|
|
600
609
|
raise NotImplementedError("Subclasses must implement get_shared_dependencies")
|
|
601
610
|
|
|
602
|
-
def get_watched_prefixes(self, package_name: str) ->
|
|
611
|
+
def get_watched_prefixes(self, package_name: str) -> tuple[str, ...]:
|
|
603
612
|
"""Get watched prefixes from manifest (from IManifestLoader)."""
|
|
604
613
|
raise NotImplementedError("Subclasses must implement get_watched_prefixes")
|
|
605
614
|
|
|
@@ -662,12 +671,12 @@ class APackageManagerStrategy(IPackageManagerStrategy, ABC):
|
|
|
662
671
|
...
|
|
663
672
|
|
|
664
673
|
@abstractmethod
|
|
665
|
-
def discover_dependencies(self) ->
|
|
674
|
+
def discover_dependencies(self) -> dict[str, str]:
|
|
666
675
|
"""Discover dependencies."""
|
|
667
676
|
...
|
|
668
677
|
|
|
669
678
|
@abstractmethod
|
|
670
|
-
def check_security_policy(self, package_name: str) ->
|
|
679
|
+
def check_security_policy(self, package_name: str) -> tuple[bool, str]:
|
|
671
680
|
"""Check security policy."""
|
|
672
681
|
...
|
|
673
682
|
|
|
@@ -683,7 +692,7 @@ class AInstallExecutionStrategy(IInstallExecutionStrategy, ABC):
|
|
|
683
692
|
"""
|
|
684
693
|
|
|
685
694
|
@abstractmethod
|
|
686
|
-
def execute_install(self, package_name: str, policy_args:
|
|
695
|
+
def execute_install(self, package_name: str, policy_args: list[str]) -> Any:
|
|
687
696
|
"""Execute installation of a package."""
|
|
688
697
|
...
|
|
689
698
|
|
|
@@ -714,7 +723,7 @@ class AInstallTimingStrategy(IInstallTimingStrategy, ABC):
|
|
|
714
723
|
...
|
|
715
724
|
|
|
716
725
|
@abstractmethod
|
|
717
|
-
def get_install_priority(self, packages:
|
|
726
|
+
def get_install_priority(self, packages: list[str]) -> list[str]:
|
|
718
727
|
"""Get priority order for installing packages."""
|
|
719
728
|
...
|
|
720
729
|
|
|
@@ -730,7 +739,7 @@ class ADiscoveryStrategy(IDiscoveryStrategy, ABC):
|
|
|
730
739
|
"""
|
|
731
740
|
|
|
732
741
|
@abstractmethod
|
|
733
|
-
def discover(self, project_root: Any) ->
|
|
742
|
+
def discover(self, project_root: Any) -> dict[str, str]:
|
|
734
743
|
"""Discover dependencies from sources."""
|
|
735
744
|
...
|
|
736
745
|
|
|
@@ -751,12 +760,12 @@ class APolicyStrategy(IPolicyStrategy, ABC):
|
|
|
751
760
|
"""
|
|
752
761
|
|
|
753
762
|
@abstractmethod
|
|
754
|
-
def is_allowed(self, package_name: str) ->
|
|
763
|
+
def is_allowed(self, package_name: str) -> tuple[bool, str]:
|
|
755
764
|
"""Check if package is allowed to be installed."""
|
|
756
765
|
...
|
|
757
766
|
|
|
758
767
|
@abstractmethod
|
|
759
|
-
def get_pip_args(self, package_name: str) ->
|
|
768
|
+
def get_pip_args(self, package_name: str) -> list[str]:
|
|
760
769
|
"""Get pip arguments based on policy."""
|
|
761
770
|
...
|
|
762
771
|
|
|
@@ -777,7 +786,7 @@ class AMappingStrategy(IMappingStrategy, ABC):
|
|
|
777
786
|
...
|
|
778
787
|
|
|
779
788
|
@abstractmethod
|
|
780
|
-
def map_package_to_imports(self, package_name: str) ->
|
|
789
|
+
def map_package_to_imports(self, package_name: str) -> list[str]:
|
|
781
790
|
"""Map package name to possible import names."""
|
|
782
791
|
...
|
|
783
792
|
|
|
@@ -794,5 +803,61 @@ __all__ = [
|
|
|
794
803
|
'ADiscoveryStrategy',
|
|
795
804
|
'APolicyStrategy',
|
|
796
805
|
'AMappingStrategy',
|
|
806
|
+
# Enhanced Strategy Interfaces for Runtime Swapping
|
|
807
|
+
'AInstallStrategy',
|
|
797
808
|
]
|
|
798
809
|
|
|
810
|
+
# =============================================================================
|
|
811
|
+
# ABSTRACT INSTALLATION STRATEGY (Enhanced for Runtime Swapping)
|
|
812
|
+
# =============================================================================
|
|
813
|
+
|
|
814
|
+
class AInstallStrategy(IInstallStrategy, ABC):
|
|
815
|
+
"""
|
|
816
|
+
Abstract base class for installation strategies.
|
|
817
|
+
|
|
818
|
+
Enables runtime strategy swapping for different installation methods
|
|
819
|
+
(pip, wheel, async, cached, etc.).
|
|
820
|
+
"""
|
|
821
|
+
|
|
822
|
+
@abstractmethod
|
|
823
|
+
def install(self, package_name: str, version: Optional[str] = None) -> bool:
|
|
824
|
+
"""
|
|
825
|
+
Install a package.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
package_name: Package name to install
|
|
829
|
+
version: Optional version specification
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
True if installation successful, False otherwise
|
|
833
|
+
"""
|
|
834
|
+
...
|
|
835
|
+
|
|
836
|
+
def can_install(self, package_name: str) -> bool:
|
|
837
|
+
"""
|
|
838
|
+
Check if this strategy can install a package.
|
|
839
|
+
|
|
840
|
+
Default implementation returns True.
|
|
841
|
+
Override for strategy-specific logic.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
package_name: Package name to check
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
True if can install, False otherwise
|
|
848
|
+
"""
|
|
849
|
+
return True
|
|
850
|
+
|
|
851
|
+
@abstractmethod
|
|
852
|
+
def uninstall(self, package_name: str) -> bool:
|
|
853
|
+
"""
|
|
854
|
+
Uninstall a package.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
package_name: Package name to uninstall
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
True if uninstallation successful, False otherwise
|
|
861
|
+
"""
|
|
862
|
+
...
|
|
863
|
+
|
exonware/xwlazy/package/conf.py
CHANGED
|
@@ -23,7 +23,7 @@ import subprocess
|
|
|
23
23
|
import sys
|
|
24
24
|
import types
|
|
25
25
|
import warnings
|
|
26
|
-
from typing import Any,
|
|
26
|
+
from typing import Any, Optional
|
|
27
27
|
|
|
28
28
|
# Import from new structure
|
|
29
29
|
from .services.host_packages import refresh_host_package
|
|
@@ -56,14 +56,14 @@ class _PackageConfig:
|
|
|
56
56
|
"""Enable/disable lazy mode for this package."""
|
|
57
57
|
if value:
|
|
58
58
|
# Default to "smart" mode when enabling lazy install
|
|
59
|
-
config_package_lazy_install_enabled
|
|
60
|
-
|
|
59
|
+
# config_package_lazy_install_enabled will register package and install global hook
|
|
60
|
+
config_package_lazy_install_enabled(self._package_name, True, mode="smart", install_hook=True)
|
|
61
61
|
refresh_host_package(self._package_name)
|
|
62
62
|
else:
|
|
63
63
|
config_package_lazy_install_enabled(self._package_name, False, install_hook=False)
|
|
64
64
|
uninstall_import_hook(self._package_name)
|
|
65
65
|
|
|
66
|
-
def lazy_install_status(self) ->
|
|
66
|
+
def lazy_install_status(self) -> dict[str, Any]:
|
|
67
67
|
"""Return runtime status for this package."""
|
|
68
68
|
return {
|
|
69
69
|
"package": self._package_name,
|
|
@@ -112,7 +112,7 @@ class _LazyConfModule(types.ModuleType):
|
|
|
112
112
|
|
|
113
113
|
def __init__(self, name: str, doc: Optional[str]) -> None:
|
|
114
114
|
super().__init__(name, doc)
|
|
115
|
-
self._package_configs:
|
|
115
|
+
self._package_configs: dict[str, _PackageConfig] = {}
|
|
116
116
|
self._suppress_warnings: bool = True # Default: suppress warnings
|
|
117
117
|
self._original_stderr: Optional[Any] = None
|
|
118
118
|
self._filtered_stderr: Optional[_FilteredStderr] = None
|
|
@@ -168,7 +168,7 @@ class _LazyConfModule(types.ModuleType):
|
|
|
168
168
|
except Exception as exc: # pragma: no cover
|
|
169
169
|
print(f"[WARN] Could not uninstall exonware-xwlazy: {exc}")
|
|
170
170
|
|
|
171
|
-
def _get_global_lazy_status(self) ->
|
|
171
|
+
def _get_global_lazy_status(self) -> dict[str, Any]:
|
|
172
172
|
"""Return aggregate status for DX tooling."""
|
|
173
173
|
installed = self._is_xwlazy_installed()
|
|
174
174
|
# Check all known packages, not just those in _package_configs
|