exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.20__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 (96) hide show
  1. exonware/__init__.py +26 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +56 -0
  5. exonware/xwlazy/common/cache.py +504 -0
  6. exonware/xwlazy/common/logger.py +257 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +232 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +165 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +245 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +283 -0
  12. exonware/xwlazy/common/services/spec_cache.py +165 -0
  13. xwlazy/lazy/lazy_state.py → exonware/xwlazy/common/services/state_manager.py +0 -2
  14. exonware/xwlazy/common/strategies/__init__.py +28 -0
  15. exonware/xwlazy/common/strategies/caching_dict.py +44 -0
  16. exonware/xwlazy/common/strategies/caching_installation.py +88 -0
  17. exonware/xwlazy/common/strategies/caching_lfu.py +66 -0
  18. exonware/xwlazy/common/strategies/caching_lru.py +63 -0
  19. exonware/xwlazy/common/strategies/caching_multitier.py +59 -0
  20. exonware/xwlazy/common/strategies/caching_ttl.py +59 -0
  21. {xwlazy/lazy → exonware/xwlazy}/config.py +51 -21
  22. exonware/xwlazy/contracts.py +1396 -0
  23. exonware/xwlazy/defs.py +378 -0
  24. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -16
  25. exonware/xwlazy/facade.py +991 -0
  26. exonware/xwlazy/module/__init__.py +18 -0
  27. exonware/xwlazy/module/base.py +565 -0
  28. exonware/xwlazy/module/data.py +17 -0
  29. exonware/xwlazy/module/facade.py +246 -0
  30. exonware/xwlazy/module/importer_engine.py +2117 -0
  31. exonware/xwlazy/module/strategies/__init__.py +22 -0
  32. exonware/xwlazy/module/strategies/module_helper_lazy.py +93 -0
  33. exonware/xwlazy/module/strategies/module_helper_simple.py +65 -0
  34. exonware/xwlazy/module/strategies/module_manager_advanced.py +111 -0
  35. exonware/xwlazy/module/strategies/module_manager_simple.py +95 -0
  36. exonware/xwlazy/package/__init__.py +18 -0
  37. exonware/xwlazy/package/base.py +798 -0
  38. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +61 -16
  39. exonware/xwlazy/package/data.py +17 -0
  40. exonware/xwlazy/package/facade.py +480 -0
  41. exonware/xwlazy/package/services/__init__.py +84 -0
  42. exonware/xwlazy/package/services/async_install_handle.py +87 -0
  43. exonware/xwlazy/package/services/config_manager.py +245 -0
  44. exonware/xwlazy/package/services/discovery.py +370 -0
  45. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -20
  46. exonware/xwlazy/package/services/install_async.py +277 -0
  47. exonware/xwlazy/package/services/install_cache.py +145 -0
  48. exonware/xwlazy/package/services/install_interactive.py +59 -0
  49. exonware/xwlazy/package/services/install_policy.py +156 -0
  50. exonware/xwlazy/package/services/install_registry.py +54 -0
  51. exonware/xwlazy/package/services/install_result.py +17 -0
  52. exonware/xwlazy/package/services/install_sbom.py +153 -0
  53. exonware/xwlazy/package/services/install_utils.py +79 -0
  54. exonware/xwlazy/package/services/installer_engine.py +406 -0
  55. exonware/xwlazy/package/services/lazy_installer.py +718 -0
  56. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +40 -33
  57. exonware/xwlazy/package/services/strategy_registry.py +186 -0
  58. exonware/xwlazy/package/strategies/__init__.py +57 -0
  59. exonware/xwlazy/package/strategies/package_discovery_file.py +129 -0
  60. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +84 -0
  61. exonware/xwlazy/package/strategies/package_discovery_manifest.py +101 -0
  62. exonware/xwlazy/package/strategies/package_execution_async.py +113 -0
  63. exonware/xwlazy/package/strategies/package_execution_cached.py +90 -0
  64. exonware/xwlazy/package/strategies/package_execution_pip.py +99 -0
  65. exonware/xwlazy/package/strategies/package_execution_wheel.py +106 -0
  66. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +100 -0
  67. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +105 -0
  68. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +100 -0
  69. exonware/xwlazy/package/strategies/package_policy_allow_list.py +57 -0
  70. exonware/xwlazy/package/strategies/package_policy_deny_list.py +57 -0
  71. exonware/xwlazy/package/strategies/package_policy_permissive.py +46 -0
  72. exonware/xwlazy/package/strategies/package_timing_clean.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_full.py +66 -0
  74. exonware/xwlazy/package/strategies/package_timing_smart.py +68 -0
  75. exonware/xwlazy/package/strategies/package_timing_temporary.py +66 -0
  76. exonware/xwlazy/runtime/__init__.py +18 -0
  77. exonware/xwlazy/runtime/adaptive_learner.py +129 -0
  78. exonware/xwlazy/runtime/base.py +274 -0
  79. exonware/xwlazy/runtime/facade.py +94 -0
  80. exonware/xwlazy/runtime/intelligent_selector.py +170 -0
  81. exonware/xwlazy/runtime/metrics.py +60 -0
  82. exonware/xwlazy/runtime/performance.py +37 -0
  83. exonware/xwlazy/version.py +2 -2
  84. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/METADATA +89 -11
  85. exonware_xwlazy-0.1.0.20.dist-info/RECORD +87 -0
  86. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  87. xwlazy/__init__.py +0 -34
  88. xwlazy/lazy/__init__.py +0 -301
  89. xwlazy/lazy/bootstrap.py +0 -106
  90. xwlazy/lazy/lazy_base.py +0 -465
  91. xwlazy/lazy/lazy_contracts.py +0 -290
  92. xwlazy/lazy/lazy_core.py +0 -3727
  93. xwlazy/lazy/logging_utils.py +0 -194
  94. xwlazy/version.py +0 -77
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,18 @@
1
- """Host-facing configuration helpers for enabling lazy mode via `exonware.conf`.
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/package/conf.py
3
+
4
+ Host-facing configuration helpers for enabling lazy mode via `exonware.conf`.
2
5
 
3
6
  This module centralizes the legacy configuration surface so host packages no
4
7
  longer need to ship their own lazy bootstrap logic. Consumers import
5
8
  ``exonware.conf`` as before, while the real implementation now lives in
6
- ``xwlazy.lazy.host_conf`` to keep lazy concerns within the xwlazy project.
9
+ ``xwlazy.package.conf`` to keep lazy concerns within the xwlazy project.
10
+
11
+ Company: eXonware.com
12
+ Author: Eng. Muhammad AlShehri
13
+ Email: connect@exonware.com
14
+
15
+ Generation Date: 10-Oct-2025
7
16
  """
8
17
 
9
18
  from __future__ import annotations
@@ -16,15 +25,19 @@ import types
16
25
  import warnings
17
26
  from typing import Any, Dict, Optional
18
27
 
19
- from .host_packages import refresh_host_package
20
- from .lazy_core import (
28
+ # Import from new structure
29
+ from .services.host_packages import refresh_host_package
30
+ from ..facade import (
21
31
  config_package_lazy_install_enabled,
22
32
  install_import_hook,
23
33
  uninstall_import_hook,
24
34
  is_import_hook_installed,
25
35
  is_lazy_install_enabled,
26
36
  )
37
+ from ..defs import get_preset_mode
38
+ from .services.config_manager import LazyInstallConfig
27
39
 
40
+ __all__ = ['get_conf_module', '_PackageConfig', '_FilteredStderr', '_LazyConfModule', '_setup_global_warning_filter']
28
41
 
29
42
  class _PackageConfig:
30
43
  """Per-package configuration wrapper."""
@@ -42,7 +55,8 @@ class _PackageConfig:
42
55
  def lazy_install(self, value: bool) -> None:
43
56
  """Enable/disable lazy mode for this package."""
44
57
  if value:
45
- config_package_lazy_install_enabled(self._package_name, True, install_hook=False)
58
+ # Default to "smart" mode when enabling lazy install
59
+ config_package_lazy_install_enabled(self._package_name, True, mode="smart", install_hook=False)
46
60
  install_import_hook(self._package_name)
47
61
  refresh_host_package(self._package_name)
48
62
  else:
@@ -63,7 +77,6 @@ class _PackageConfig:
63
77
  """Return True if lazy install + hook are active."""
64
78
  return self.lazy_install_status()["active"]
65
79
 
66
-
67
80
  class _FilteredStderr:
68
81
  """Stderr wrapper that filters out specific warning messages."""
69
82
 
@@ -94,7 +107,6 @@ class _FilteredStderr:
94
107
  """Delegate all other attributes to original stderr."""
95
108
  return getattr(self._original, name)
96
109
 
97
-
98
110
  class _LazyConfModule(types.ModuleType):
99
111
  """Configuration module for all exonware packages."""
100
112
 
@@ -120,7 +132,7 @@ class _LazyConfModule(types.ModuleType):
120
132
  return False
121
133
  except Exception:
122
134
  try:
123
- import xwlazy # noqa: F401
135
+ import exonware.xwlazy # noqa: F401
124
136
  return True
125
137
  except ImportError:
126
138
  return False
@@ -205,6 +217,21 @@ class _LazyConfModule(types.ModuleType):
205
217
 
206
218
  if name == "lazy_install":
207
219
  return self._is_xwlazy_installed()
220
+ if name == "lazy":
221
+ # Return current lazy mode setting
222
+ # Check if any package has lazy enabled and return its mode
223
+ for pkg_name in package_names:
224
+ if is_lazy_install_enabled(pkg_name):
225
+ mode_config = LazyInstallConfig.get_mode_config(pkg_name)
226
+ if mode_config:
227
+ # Return preset name if matches, otherwise return mode string
228
+ from ..defs import PRESET_MODES
229
+ for preset_name, preset_config in PRESET_MODES.items():
230
+ if (preset_config.load_mode == mode_config.load_mode and
231
+ preset_config.install_mode == mode_config.install_mode):
232
+ return preset_name
233
+ return LazyInstallConfig.get_mode(pkg_name)
234
+ return "none"
208
235
  if name == "lazy_install_status":
209
236
  return self._get_global_lazy_status
210
237
  if name == "is_lazy_active":
@@ -221,23 +248,45 @@ class _LazyConfModule(types.ModuleType):
221
248
  if name == "lazy_install":
222
249
  if value:
223
250
  self._ensure_xwlazy_installed()
224
- self.__getattr__("xwsystem").lazy_install = True
251
+ # Enable with "smart" mode by default
252
+ package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
253
+ for pkg_name in package_names:
254
+ config_package_lazy_install_enabled(pkg_name, True, mode="smart", install_hook=True)
225
255
  else:
226
- self.__getattr__("xwsystem").lazy_install = False
256
+ package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
257
+ for pkg_name in package_names:
258
+ config_package_lazy_install_enabled(pkg_name, False, install_hook=False)
259
+ uninstall_import_hook(pkg_name)
227
260
  self._uninstall_xwlazy()
228
261
  return
262
+ if name == "lazy":
263
+ # Support exonware.conf.lazy = "lite"/"smart"/"full"/"clean"/"auto"
264
+ mode_map = {
265
+ "lite": "lite",
266
+ "smart": "smart",
267
+ "full": "full",
268
+ "clean": "clean",
269
+ "auto": "auto",
270
+ "temporary": "temporary",
271
+ "size_aware": "size_aware",
272
+ "none": "none",
273
+ }
274
+ mode = mode_map.get(str(value).lower(), "smart") # Default to "smart" instead of "auto"
275
+ # Apply to all known packages
276
+ package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
277
+ for pkg_name in package_names:
278
+ config_package_lazy_install_enabled(pkg_name, True, mode, install_hook=True)
279
+ return
229
280
  if name == "suppress_warnings":
230
281
  self._suppress_warnings = bool(value)
231
282
  self._setup_warning_filter()
232
283
  return
233
284
  super().__setattr__(name, value)
234
285
 
235
-
236
286
  _CONF_INSTANCE: Optional[_LazyConfModule] = None
237
287
  _ORIGINAL_STDERR: Optional[Any] = None
238
288
  _FILTERED_STDERR: Optional[_FilteredStderr] = None
239
289
 
240
-
241
290
  def _setup_global_warning_filter() -> None:
242
291
  """Set up global stderr filter for decimal module warnings (called at module import)."""
243
292
  global _ORIGINAL_STDERR, _FILTERED_STDERR
@@ -263,17 +312,13 @@ def _setup_global_warning_filter() -> None:
263
312
  if sys.stderr is not _FILTERED_STDERR:
264
313
  sys.stderr = _FILTERED_STDERR # type: ignore[assignment]
265
314
 
266
-
267
315
  # Set up warning filter immediately when module is imported (default: suppress warnings)
268
316
  # Note: conf.py may have already set up a filter, which is fine
269
317
  _setup_global_warning_filter()
270
318
 
271
-
272
319
  def get_conf_module(name: str = "exonware.conf", doc: Optional[str] = None) -> types.ModuleType:
273
320
  """Return (and memoize) the shared conf module instance."""
274
321
  global _CONF_INSTANCE
275
322
  if _CONF_INSTANCE is None:
276
323
  _CONF_INSTANCE = _LazyConfModule(name, doc)
277
324
  return _CONF_INSTANCE
278
-
279
-
@@ -0,0 +1,17 @@
1
+ """
2
+ Package Data - Immutable data structure for packages.
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Re-export PackageData from defs.py for backward compatibility.
11
+ """
12
+
13
+ # Re-export from defs.py
14
+ from ..defs import PackageData
15
+
16
+ __all__ = ['PackageData']
17
+
@@ -0,0 +1,480 @@
1
+ """
2
+ Package Operations Facade
3
+
4
+ Main facade: XWPackageHelper extends APackageHelper
5
+ Provides concrete implementation for all package operations.
6
+ Uses strategy pattern for caching, helper, and manager strategies.
7
+ """
8
+
9
+ import sys
10
+ import subprocess
11
+ import importlib
12
+ import importlib.util
13
+ from typing import Optional
14
+
15
+ from .base import APackageHelper
16
+ from .services import InstallerEngine, LazyInstaller
17
+ from .services.strategy_registry import StrategyRegistry
18
+ from ..common.cache import InstallationCache
19
+ from ..common.logger import get_logger
20
+
21
+ logger = get_logger("xwlazy.package.facade")
22
+
23
+ # Import strategy interfaces
24
+ from ..contracts import (
25
+ ICachingStrategy,
26
+ IPackageHelperStrategy,
27
+ IPackageManagerStrategy,
28
+ IInstallExecutionStrategy,
29
+ IInstallTimingStrategy,
30
+ IDiscoveryStrategy,
31
+ IPolicyStrategy,
32
+ IMappingStrategy,
33
+ )
34
+
35
+ # Import default strategies
36
+ from ..common.strategies import InstallationCacheWrapper
37
+ from .strategies import (
38
+ # Execution strategies
39
+ PipExecution,
40
+ # Timing strategies
41
+ SmartTiming,
42
+ # Discovery strategies
43
+ HybridDiscovery,
44
+ # Policy strategies
45
+ PermissivePolicy,
46
+ # Mapping strategies
47
+ ManifestFirstMapping,
48
+ )
49
+
50
+ class XWPackageHelper(APackageHelper):
51
+ """
52
+ Concrete implementation of APackageHelper.
53
+
54
+ Provides simple, clean API for working with packages (what you pip install).
55
+ Uses xwlazy's InstallationCache for persistent caching and LazyInstaller for installation.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ package_name: str = 'default',
61
+ project_root: Optional[str] = None,
62
+ *,
63
+ # Legacy strategy injection (for backward compatibility)
64
+ caching_strategy: Optional[ICachingStrategy] = None,
65
+ helper_strategy: Optional[IPackageHelperStrategy] = None,
66
+ manager_strategy: Optional[IPackageManagerStrategy] = None,
67
+ # New strategy types
68
+ execution_strategy: Optional[IInstallExecutionStrategy] = None,
69
+ timing_strategy: Optional[IInstallTimingStrategy] = None,
70
+ discovery_strategy: Optional[IDiscoveryStrategy] = None,
71
+ policy_strategy: Optional[IPolicyStrategy] = None,
72
+ mapping_strategy: Optional[IMappingStrategy] = None,
73
+ ):
74
+ """
75
+ Initialize XW package helper.
76
+
77
+ Args:
78
+ package_name: Package name for isolation (defaults to 'default')
79
+ project_root: Root directory of project (auto-detected if None)
80
+ caching_strategy: Optional caching strategy. If None, uses InstallationCache.
81
+ helper_strategy: Optional helper strategy (legacy, deprecated).
82
+ manager_strategy: Optional manager strategy (legacy, deprecated).
83
+ execution_strategy: Optional execution strategy. If None, uses PipExecution.
84
+ timing_strategy: Optional timing strategy. If None, uses SmartTiming.
85
+ discovery_strategy: Optional discovery strategy. If None, uses HybridDiscovery.
86
+ policy_strategy: Optional policy strategy. If None, uses PermissivePolicy.
87
+ mapping_strategy: Optional mapping strategy. If None, uses ManifestFirstMapping.
88
+ """
89
+ super().__init__(package_name, project_root)
90
+
91
+ # Default strategies (legacy - deprecated, kept for backward compatibility)
92
+ if caching_strategy is None:
93
+ caching_strategy = InstallationCacheWrapper()
94
+ # Legacy helper_strategy and manager_strategy are deprecated
95
+ # They are kept for backward compatibility but not used
96
+
97
+ # Check registry for stored strategies, otherwise use defaults
98
+ if execution_strategy is None:
99
+ execution_strategy = StrategyRegistry.get_package_strategy(package_name, 'execution')
100
+ if execution_strategy is None:
101
+ execution_strategy = PipExecution()
102
+ if timing_strategy is None:
103
+ timing_strategy = StrategyRegistry.get_package_strategy(package_name, 'timing')
104
+ if timing_strategy is None:
105
+ timing_strategy = SmartTiming()
106
+ if discovery_strategy is None:
107
+ discovery_strategy = StrategyRegistry.get_package_strategy(package_name, 'discovery')
108
+ if discovery_strategy is None:
109
+ discovery_strategy = HybridDiscovery(package_name, project_root)
110
+ if policy_strategy is None:
111
+ policy_strategy = StrategyRegistry.get_package_strategy(package_name, 'policy')
112
+ if policy_strategy is None:
113
+ policy_strategy = PermissivePolicy()
114
+ if mapping_strategy is None:
115
+ mapping_strategy = StrategyRegistry.get_package_strategy(package_name, 'mapping')
116
+ if mapping_strategy is None:
117
+ mapping_strategy = ManifestFirstMapping(package_name)
118
+
119
+ # Store strategies
120
+ self._caching = caching_strategy
121
+ self._helper = helper_strategy # Legacy, deprecated
122
+ self._manager = manager_strategy # Legacy, deprecated
123
+ self._execution = execution_strategy
124
+ self._timing = timing_strategy
125
+ self._discovery = discovery_strategy
126
+ self._policy = policy_strategy
127
+ self._mapping = mapping_strategy
128
+
129
+ # Legacy support (for backward compatibility)
130
+ self._install_cache = InstallationCache()
131
+ self._installer = None # Lazy init to avoid circular imports
132
+ self._install_engine = InstallerEngine(package_name)
133
+
134
+ def _get_installer(self):
135
+ """Get lazy installer instance (lazy init)."""
136
+ if self._installer is None:
137
+ self._installer = LazyInstaller(self._package_name)
138
+ return self._installer
139
+
140
+ def _check_importability(self, package_name: str) -> bool:
141
+ """
142
+ Check if package is importable.
143
+
144
+ Uses importlib.util.find_spec to check if package can be imported.
145
+
146
+ Args:
147
+ package_name: Package name to check
148
+
149
+ Returns:
150
+ True if importable, False otherwise
151
+ """
152
+ try:
153
+ spec = importlib.util.find_spec(package_name)
154
+ return spec is not None
155
+ except (ValueError, AttributeError, ImportError):
156
+ return False
157
+
158
+ def _check_persistent_cache(self, package_name: str) -> bool:
159
+ """
160
+ Check persistent cache for package installation status.
161
+
162
+ Args:
163
+ package_name: Package name to check
164
+
165
+ Returns:
166
+ True if found in persistent cache as installed, False otherwise
167
+ """
168
+ return self._install_cache.is_installed(package_name)
169
+
170
+ def _mark_installed_in_persistent_cache(self, package_name: str) -> None:
171
+ """
172
+ Mark package as installed in persistent cache.
173
+
174
+ Args:
175
+ package_name: Package name to mark
176
+ """
177
+ version = self._get_installer()._get_installed_version(package_name)
178
+ self._install_cache.mark_installed(package_name, version)
179
+
180
+ def _mark_uninstalled_in_persistent_cache(self, package_name: str) -> None:
181
+ """
182
+ Mark package as uninstalled in persistent cache.
183
+
184
+ Args:
185
+ package_name: Package name to mark
186
+ """
187
+ self._install_cache.mark_uninstalled(package_name)
188
+
189
+ def _run_install(self, *package_names: str) -> None:
190
+ """
191
+ Run pip install for packages.
192
+
193
+ Uses execution strategy and timing strategy to determine when/how to install.
194
+
195
+ Args:
196
+ *package_names: Package names to install
197
+
198
+ Raises:
199
+ RuntimeError: If installation fails
200
+ """
201
+ if not package_names:
202
+ return
203
+
204
+ # Get policy args for each package
205
+ policy_args_map = {}
206
+ for package_name in package_names:
207
+ # Check if should install now (timing strategy)
208
+ if not self._timing.should_install_now(package_name, None):
209
+ continue
210
+
211
+ # Get pip args from policy strategy
212
+ policy_args = self._policy.get_pip_args(package_name)
213
+ policy_args_map[package_name] = policy_args
214
+
215
+ # Execute installations using execution strategy
216
+ for package_name, policy_args in policy_args_map.items():
217
+ result = self._execution.execute_install(package_name, policy_args)
218
+
219
+ # Handle result
220
+ if hasattr(result, 'success') and result.success:
221
+ with self._lock:
222
+ self._installed_packages.add(package_name)
223
+ self._uninstalled_packages.discard(package_name)
224
+ self._mark_installed_in_persistent_cache(package_name)
225
+ else:
226
+ with self._lock:
227
+ self._failed_packages.add(package_name)
228
+ error_msg = getattr(result, 'error', 'Unknown error') if hasattr(result, 'error') else str(result)
229
+ raise RuntimeError(f"Failed to install {package_name}: {error_msg}")
230
+
231
+ def _run_uninstall(self, *package_names: str) -> None:
232
+ """
233
+ Run pip uninstall for packages.
234
+
235
+ Uses execution strategy for uninstallation.
236
+
237
+ Args:
238
+ *package_names: Package names to uninstall
239
+
240
+ Raises:
241
+ RuntimeError: If uninstallation fails
242
+ """
243
+ if not package_names:
244
+ return
245
+
246
+ for package_name in package_names:
247
+ # Check if should uninstall (timing strategy)
248
+ if self._timing.should_uninstall_after(package_name, None):
249
+ success = self._execution.execute_uninstall(package_name)
250
+ if success:
251
+ with self._lock:
252
+ self._installed_packages.discard(package_name)
253
+ self._uninstalled_packages.add(package_name)
254
+ self._mark_uninstalled_in_persistent_cache(package_name)
255
+ else:
256
+ raise RuntimeError(f"Failed to uninstall {package_name}")
257
+
258
+ # Abstract methods from APackage that need implementation
259
+ def _discover_from_sources(self) -> None:
260
+ """Discover dependencies from all sources."""
261
+ # Use discovery strategy
262
+ deps = self._discovery.discover(self._project_root)
263
+ # Convert to DependencyInfo format
264
+ from ..defs import DependencyInfo
265
+ for import_name, package_name in deps.items():
266
+ self.discovered_dependencies[import_name] = DependencyInfo(
267
+ import_name=import_name,
268
+ package_name=package_name,
269
+ source=self._discovery.get_source(import_name) or 'discovery',
270
+ category='discovered'
271
+ )
272
+
273
+ def _is_cache_valid(self) -> bool:
274
+ """Check if cached dependencies are still valid."""
275
+ # Delegate to discovery strategy which has cache validation logic
276
+ if hasattr(self._discovery, '_is_cache_valid'):
277
+ return self._discovery._is_cache_valid()
278
+ # Fallback: if discovery doesn't support cache validation, assume invalid
279
+ return False
280
+
281
+ def _add_common_mappings(self) -> None:
282
+ """Add common import -> package mappings."""
283
+ # Use mapping strategy to discover mappings
284
+ # This is called during initialization to populate common mappings
285
+ # The mapping strategy handles this internally
286
+ pass
287
+
288
+ def _update_file_mtimes(self) -> None:
289
+ """Update file modification times for cache validation."""
290
+ # Delegate to discovery strategy which tracks file modification times
291
+ if hasattr(self._discovery, '_update_file_mtimes'):
292
+ self._discovery._update_file_mtimes()
293
+
294
+ # Strategy swapping methods
295
+ def swap_cache_strategy(self, new_strategy: ICachingStrategy) -> None:
296
+ """
297
+ Swap cache strategy at runtime.
298
+
299
+ Args:
300
+ new_strategy: New caching strategy to use
301
+ """
302
+ self._caching = new_strategy
303
+ # Update manager if it uses caching
304
+ if hasattr(self._manager, '_caching'):
305
+ self._manager._caching = new_strategy
306
+
307
+ def swap_helper_strategy(self, new_strategy: IPackageHelperStrategy) -> None:
308
+ """
309
+ Swap helper/installer strategy at runtime.
310
+
311
+ Args:
312
+ new_strategy: New helper strategy to use
313
+ """
314
+ self._helper = new_strategy
315
+ # Update manager if it uses helper
316
+ if hasattr(self._manager, '_helper'):
317
+ self._manager._helper = new_strategy
318
+
319
+ def swap_manager_strategy(self, new_strategy: IPackageManagerStrategy) -> None:
320
+ """
321
+ Swap manager strategy at runtime.
322
+
323
+ Args:
324
+ new_strategy: New manager strategy to use
325
+ """
326
+ self._manager = new_strategy
327
+
328
+ def swap_execution_strategy(self, new_strategy: IInstallExecutionStrategy) -> None:
329
+ """
330
+ Swap execution strategy at runtime.
331
+
332
+ Args:
333
+ new_strategy: New execution strategy to use
334
+ """
335
+ self._execution = new_strategy
336
+
337
+ def swap_timing_strategy(self, new_strategy: IInstallTimingStrategy) -> None:
338
+ """
339
+ Swap timing strategy at runtime.
340
+
341
+ Args:
342
+ new_strategy: New timing strategy to use
343
+ """
344
+ self._timing = new_strategy
345
+
346
+ def swap_discovery_strategy(self, new_strategy: IDiscoveryStrategy) -> None:
347
+ """
348
+ Swap discovery strategy at runtime.
349
+
350
+ Args:
351
+ new_strategy: New discovery strategy to use
352
+ """
353
+ self._discovery = new_strategy
354
+
355
+ def swap_policy_strategy(self, new_strategy: IPolicyStrategy) -> None:
356
+ """
357
+ Swap policy strategy at runtime.
358
+
359
+ Args:
360
+ new_strategy: New policy strategy to use
361
+ """
362
+ self._policy = new_strategy
363
+
364
+ def swap_mapping_strategy(self, new_strategy: IMappingStrategy) -> None:
365
+ """
366
+ Swap mapping strategy at runtime.
367
+
368
+ Args:
369
+ new_strategy: New mapping strategy to use
370
+ """
371
+ self._mapping = new_strategy
372
+
373
+ def install_package(self, package_name: str, module_name: Optional[str] = None) -> bool:
374
+ """
375
+ Install a package.
376
+
377
+ Uses timing strategy to determine if should install now,
378
+ then uses execution strategy to perform installation.
379
+
380
+ Args:
381
+ package_name: Package name to install
382
+ module_name: Optional module name (for mapping)
383
+
384
+ Returns:
385
+ True if installed successfully, False otherwise
386
+ """
387
+ # Map module name to package name if needed (using mapping strategy)
388
+ if module_name and not package_name:
389
+ package_name = self._mapping.map_import_to_package(module_name) or module_name
390
+
391
+ # Check timing strategy
392
+ if not self._timing.should_install_now(package_name, {'module_name': module_name}):
393
+ return False
394
+
395
+ # Check policy strategy
396
+ allowed, reason = self._policy.is_allowed(package_name)
397
+ if not allowed:
398
+ raise RuntimeError(f"Package {package_name} blocked by policy: {reason}")
399
+
400
+ # Get pip args from policy
401
+ policy_args = self._policy.get_pip_args(package_name)
402
+
403
+ # Execute installation
404
+ result = self._execution.execute_install(package_name, policy_args)
405
+
406
+ # Handle result
407
+ if hasattr(result, 'success') and result.success:
408
+ with self._lock:
409
+ self._installed_packages.add(package_name)
410
+ self._uninstalled_packages.discard(package_name)
411
+ self._mark_installed_in_persistent_cache(package_name)
412
+ return True
413
+ else:
414
+ with self._lock:
415
+ self._failed_packages.add(package_name)
416
+ return False
417
+
418
+ def _check_security_policy(self, package_name: str):
419
+ """Check security policy for package."""
420
+ # Use policy strategy
421
+ return self._policy.is_allowed(package_name)
422
+
423
+ def _run_pip_install(self, package_name: str, args: list) -> bool:
424
+ """Run pip install with arguments."""
425
+ try:
426
+ self._run_install(package_name)
427
+ return True
428
+ except (RuntimeError, subprocess.CalledProcessError, OSError) as e:
429
+ logger.debug(f"Failed to install {package_name}: {e}")
430
+ return False
431
+ except Exception as e:
432
+ # Catch-all for unexpected errors, but log them
433
+ logger.warning(f"Unexpected error installing {package_name}: {e}")
434
+ return False
435
+
436
+ def is_cache_valid(self, key: str) -> bool:
437
+ """Check if cache entry is still valid."""
438
+ # Use caching strategy if it supports validation
439
+ if hasattr(self._caching, 'is_valid') and self._caching is not None:
440
+ return self._caching.is_valid(key)
441
+ # Fallback: check if key exists in cache
442
+ return self.get_cached(key) is not None
443
+
444
+ # IConfigManager methods (delegate to LazyInstallConfig)
445
+ def is_enabled(self, package_name: str) -> bool:
446
+ """
447
+ Check if lazy install is enabled for a package (from IConfigManager).
448
+
449
+ This method delegates to LazyInstallConfig to avoid method name conflict
450
+ with the instance method is_enabled().
451
+
452
+ Args:
453
+ package_name: Package name to check
454
+
455
+ Returns:
456
+ True if enabled, False otherwise
457
+ """
458
+ from .services.config_manager import LazyInstallConfig
459
+ return LazyInstallConfig.is_enabled(package_name)
460
+
461
+ def get_mode(self, package_name: str) -> str:
462
+ """Get installation mode for a package (from IConfigManager)."""
463
+ from .services.config_manager import LazyInstallConfig
464
+ return LazyInstallConfig.get_mode(package_name)
465
+
466
+ def get_load_mode(self, package_name: str):
467
+ """Get load mode for a package (from IConfigManager)."""
468
+ from .services.config_manager import LazyInstallConfig
469
+ return LazyInstallConfig.get_load_mode(package_name)
470
+
471
+ def get_install_mode(self, package_name: str):
472
+ """Get install mode for a package (from IConfigManager)."""
473
+ from .services.config_manager import LazyInstallConfig
474
+ return LazyInstallConfig.get_install_mode(package_name)
475
+
476
+ def get_mode_config(self, package_name: str):
477
+ """Get full mode configuration for a package (from IConfigManager)."""
478
+ from .services.config_manager import LazyInstallConfig
479
+ return LazyInstallConfig.get_mode_config(package_name)
480
+