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.
Files changed (71) hide show
  1. exonware/__init__.py +20 -1
  2. exonware/xwlazy/__init__.py +14 -2
  3. exonware/xwlazy/common/__init__.py +8 -0
  4. exonware/xwlazy/common/base.py +11 -2
  5. exonware/xwlazy/common/cache.py +5 -5
  6. exonware/xwlazy/common/logger.py +5 -5
  7. exonware/xwlazy/common/services/dependency_mapper.py +31 -13
  8. exonware/xwlazy/common/services/install_async_utils.py +5 -0
  9. exonware/xwlazy/common/services/install_cache_utils.py +4 -4
  10. exonware/xwlazy/common/services/spec_cache.py +2 -2
  11. exonware/xwlazy/common/services/state_manager.py +4 -4
  12. exonware/xwlazy/common/strategies/caching_dict.py +2 -2
  13. exonware/xwlazy/common/strategies/caching_lfu.py +2 -2
  14. exonware/xwlazy/common/strategies/caching_ttl.py +2 -2
  15. exonware/xwlazy/common/utils.py +142 -0
  16. exonware/xwlazy/config.py +1 -1
  17. exonware/xwlazy/contracts.py +162 -25
  18. exonware/xwlazy/defs.py +15 -15
  19. exonware/xwlazy/facade.py +175 -29
  20. exonware/xwlazy/host/__init__.py +8 -0
  21. exonware/xwlazy/host/conf.py +16 -0
  22. exonware/xwlazy/module/base.py +61 -4
  23. exonware/xwlazy/module/facade.py +1 -1
  24. exonware/xwlazy/module/importer_engine.py +1017 -170
  25. exonware/xwlazy/module/partial_module_detector.py +275 -0
  26. exonware/xwlazy/module/strategies/module_helper_lazy.py +3 -3
  27. exonware/xwlazy/package/base.py +106 -41
  28. exonware/xwlazy/package/conf.py +6 -6
  29. exonware/xwlazy/package/services/config_manager.py +20 -16
  30. exonware/xwlazy/package/services/discovery.py +81 -16
  31. exonware/xwlazy/package/services/host_packages.py +41 -6
  32. exonware/xwlazy/package/services/install_async.py +16 -2
  33. exonware/xwlazy/package/services/install_cache.py +4 -4
  34. exonware/xwlazy/package/services/install_policy.py +14 -14
  35. exonware/xwlazy/package/services/install_registry.py +3 -3
  36. exonware/xwlazy/package/services/install_sbom.py +1 -1
  37. exonware/xwlazy/package/services/installer_engine.py +3 -3
  38. exonware/xwlazy/package/services/lazy_installer.py +102 -17
  39. exonware/xwlazy/package/services/manifest.py +43 -36
  40. exonware/xwlazy/package/services/strategy_registry.py +150 -12
  41. exonware/xwlazy/package/strategies/package_discovery_file.py +2 -2
  42. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +2 -2
  43. exonware/xwlazy/package/strategies/package_discovery_manifest.py +2 -2
  44. exonware/xwlazy/package/strategies/package_execution_async.py +3 -3
  45. exonware/xwlazy/package/strategies/package_execution_cached.py +2 -2
  46. exonware/xwlazy/package/strategies/package_execution_pip.py +2 -2
  47. exonware/xwlazy/package/strategies/package_execution_wheel.py +2 -2
  48. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +2 -2
  49. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +2 -2
  50. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +2 -2
  51. exonware/xwlazy/package/strategies/package_policy_allow_list.py +4 -4
  52. exonware/xwlazy/package/strategies/package_policy_deny_list.py +4 -4
  53. exonware/xwlazy/package/strategies/package_policy_permissive.py +3 -3
  54. exonware/xwlazy/package/strategies/package_timing_clean.py +2 -2
  55. exonware/xwlazy/package/strategies/package_timing_full.py +2 -2
  56. exonware/xwlazy/package/strategies/package_timing_smart.py +2 -2
  57. exonware/xwlazy/package/strategies/package_timing_temporary.py +2 -2
  58. exonware/xwlazy/runtime/adaptive_learner.py +7 -7
  59. exonware/xwlazy/runtime/base.py +14 -14
  60. exonware/xwlazy/runtime/facade.py +7 -7
  61. exonware/xwlazy/runtime/intelligent_selector.py +6 -6
  62. exonware/xwlazy/runtime/metrics.py +6 -6
  63. exonware/xwlazy/runtime/performance.py +5 -5
  64. exonware/xwlazy/version.py +2 -2
  65. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/METADATA +2 -6
  66. exonware_xwlazy-0.1.0.23.dist-info/RECORD +93 -0
  67. xwlazy/__init__.py +14 -0
  68. xwlazy/lazy.py +30 -0
  69. exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
  70. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/WHEEL +0 -0
  71. {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, Dict
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: Dict[str, ModuleType] = {}
32
- self._loading: Dict[str, bool] = {}
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:
@@ -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 Dict, List, Optional, Any, Set, Tuple
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: Dict[str, DependencyInfo] = {}
82
- self._discovery_sources: List[str] = []
83
- self._cached_dependencies: Dict[str, str] = {}
84
- self._file_mtimes: Dict[str, float] = {}
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: Set[str] = set()
92
- self._failed_packages: Set[str] = set()
92
+ self._installed_packages: set[str] = set()
93
+ self._failed_packages: set[str] = set()
93
94
 
94
95
  # From APackageCache
95
- self._cache: Dict[str, Any] = {}
96
+ self._cache: dict[str, Any] = {}
96
97
 
97
98
  # From APackageHelper
98
- self._uninstalled_packages: Set[str] = set()
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
- """Find the project root directory by looking for markers."""
109
- current = Path(__file__).parent.parent.parent
110
- while current != current.parent:
111
- if (current / 'pyproject.toml').exists() or (current / 'setup.py').exists():
112
- return current
113
- current = current.parent
114
- return Path.cwd()
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) -> Dict[str, str]:
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) -> List[str]:
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
- """Get the package name this instance is for."""
199
- return self._package_name
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) -> Tuple[bool, 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: List[str]) -> bool:
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) -> Dict[str, Any]:
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) -> Tuple[Optional[ModuleType], bool]:
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) -> List[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
- """Get package name for an import name (from IDependencyMapper)."""
565
- raise NotImplementedError("Subclasses must implement get_package_name")
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) -> List[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[Tuple[str, float, float]]:
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[Tuple[str, float, float]] = None) -> Dict[str, str]:
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) -> Tuple[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) -> Dict[str, str]:
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) -> Tuple[bool, 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: List[str]) -> Any:
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: List[str]) -> List[str]:
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) -> Dict[str, str]:
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) -> Tuple[bool, 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) -> List[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) -> List[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
+
@@ -23,7 +23,7 @@ import subprocess
23
23
  import sys
24
24
  import types
25
25
  import warnings
26
- from typing import Any, Dict, Optional
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(self._package_name, True, mode="smart", install_hook=False)
60
- install_import_hook(self._package_name)
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) -> Dict[str, Any]:
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: Dict[str, _PackageConfig] = {}
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) -> Dict[str, Any]:
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