exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.19__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 +22 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +58 -0
  5. exonware/xwlazy/common/cache.py +506 -0
  6. exonware/xwlazy/common/logger.py +268 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +234 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +169 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +257 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +292 -0
  12. exonware/xwlazy/common/services/spec_cache.py +173 -0
  13. exonware/xwlazy/common/strategies/__init__.py +28 -0
  14. exonware/xwlazy/common/strategies/caching_dict.py +45 -0
  15. exonware/xwlazy/common/strategies/caching_installation.py +89 -0
  16. exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
  17. exonware/xwlazy/common/strategies/caching_lru.py +64 -0
  18. exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
  19. exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
  20. {xwlazy/lazy → exonware/xwlazy}/config.py +52 -20
  21. exonware/xwlazy/contracts.py +1410 -0
  22. exonware/xwlazy/defs.py +397 -0
  23. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -8
  24. exonware/xwlazy/facade.py +1049 -0
  25. exonware/xwlazy/module/__init__.py +18 -0
  26. exonware/xwlazy/module/base.py +569 -0
  27. exonware/xwlazy/module/data.py +17 -0
  28. exonware/xwlazy/module/facade.py +247 -0
  29. exonware/xwlazy/module/importer_engine.py +2161 -0
  30. exonware/xwlazy/module/strategies/__init__.py +22 -0
  31. exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
  32. exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
  33. exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
  34. exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
  35. exonware/xwlazy/package/__init__.py +18 -0
  36. exonware/xwlazy/package/base.py +807 -0
  37. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +62 -10
  38. exonware/xwlazy/package/data.py +17 -0
  39. exonware/xwlazy/package/facade.py +481 -0
  40. exonware/xwlazy/package/services/__init__.py +84 -0
  41. exonware/xwlazy/package/services/async_install_handle.py +89 -0
  42. exonware/xwlazy/package/services/config_manager.py +246 -0
  43. exonware/xwlazy/package/services/discovery.py +374 -0
  44. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -16
  45. exonware/xwlazy/package/services/install_async.py +278 -0
  46. exonware/xwlazy/package/services/install_cache.py +146 -0
  47. exonware/xwlazy/package/services/install_interactive.py +60 -0
  48. exonware/xwlazy/package/services/install_policy.py +158 -0
  49. exonware/xwlazy/package/services/install_registry.py +56 -0
  50. exonware/xwlazy/package/services/install_result.py +17 -0
  51. exonware/xwlazy/package/services/install_sbom.py +154 -0
  52. exonware/xwlazy/package/services/install_utils.py +83 -0
  53. exonware/xwlazy/package/services/installer_engine.py +408 -0
  54. exonware/xwlazy/package/services/lazy_installer.py +720 -0
  55. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +42 -25
  56. exonware/xwlazy/package/services/strategy_registry.py +188 -0
  57. exonware/xwlazy/package/strategies/__init__.py +57 -0
  58. exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
  59. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
  60. exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
  61. exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
  62. exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
  63. exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
  64. exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
  65. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
  66. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
  67. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
  68. exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
  69. exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
  70. exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
  71. exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
  72. exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
  74. exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
  75. exonware/xwlazy/runtime/__init__.py +18 -0
  76. exonware/xwlazy/runtime/adaptive_learner.py +131 -0
  77. exonware/xwlazy/runtime/base.py +276 -0
  78. exonware/xwlazy/runtime/facade.py +95 -0
  79. exonware/xwlazy/runtime/intelligent_selector.py +173 -0
  80. exonware/xwlazy/runtime/metrics.py +64 -0
  81. exonware/xwlazy/runtime/performance.py +39 -0
  82. exonware/xwlazy/version.py +2 -2
  83. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/METADATA +86 -10
  84. exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
  85. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  86. xwlazy/__init__.py +0 -34
  87. xwlazy/lazy/__init__.py +0 -301
  88. xwlazy/lazy/bootstrap.py +0 -106
  89. xwlazy/lazy/lazy_base.py +0 -465
  90. xwlazy/lazy/lazy_contracts.py +0 -290
  91. xwlazy/lazy/lazy_core.py +0 -3727
  92. xwlazy/lazy/logging_utils.py +0 -194
  93. xwlazy/version.py +0 -77
  94. /xwlazy/lazy/lazy_state.py → /exonware/xwlazy/common/services/state_manager.py +0 -0
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1049 @@
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/facade.py
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+ Version: 0.1.0.19
8
+ Generation Date: 10-Oct-2025
9
+
10
+ Facade for Lazy Loading System
11
+
12
+ This module provides a unified public API facade for the lazy loading system
13
+ following GUIDE_ARCH.md structure. It consolidates all public APIs into a
14
+ single entry point.
15
+
16
+ Design Pattern: Facade Pattern
17
+ - Provides simplified interface to complex subsystem
18
+ - Hides implementation details
19
+ - Centralizes public API
20
+ """
21
+
22
+ import sys
23
+ import subprocess
24
+ import importlib
25
+ import importlib.util
26
+ from typing import Dict, List, Optional, Tuple, Any
27
+ from types import ModuleType
28
+
29
+ # Import from contracts for types
30
+ from .defs import LazyInstallMode, LazyLoadMode, LazyModeConfig
31
+ from .defs import PRESET_MODES, get_preset_mode
32
+
33
+ # Import from new structure modules
34
+ from .package.services.config_manager import LazyInstallConfig
35
+ from .common.services import LazyStateManager
36
+ from .runtime.metrics import MetricsCollector, get_metrics_collector
37
+ from .runtime.performance import LazyPerformanceMonitor
38
+ from .package.services.manifest import get_manifest_loader, refresh_manifest_cache
39
+ from .common.logger import get_logger, log_event as _log
40
+ # Import directly from submodule bases
41
+ from .package.base import APackageHelper
42
+ from .module.base import AModuleHelper
43
+ from .runtime.base import ARuntimeHelper
44
+ # Import concrete implementations from new folder structure
45
+ from .package import XWPackageHelper
46
+ from .module import XWModuleHelper
47
+ from .runtime import XWRuntimeHelper
48
+
49
+ # Import from domain modules
50
+ from .package.services.discovery import get_lazy_discovery as _get_lazy_discovery
51
+ from .common.services.dependency_mapper import DependencyMapper
52
+ from .common.services import (
53
+ enable_keyword_detection as _enable_keyword_detection,
54
+ is_keyword_detection_enabled as _is_keyword_detection_enabled,
55
+ get_keyword_detection_keyword as _get_keyword_detection_keyword,
56
+ check_package_keywords as _check_package_keywords,
57
+ _detect_lazy_installation,
58
+ _detect_meta_info_mode,
59
+ )
60
+ from .package.services import (
61
+ LazyInstallerRegistry,
62
+ LazyInstaller,
63
+ LazyInstallPolicy,
64
+ is_externally_managed as _is_externally_managed,
65
+ )
66
+ from .common.cache import InstallationCache
67
+ from .module.importer_engine import (
68
+ install_import_hook as _install_import_hook,
69
+ uninstall_import_hook as _uninstall_import_hook,
70
+ is_import_hook_installed as _is_import_hook_installed,
71
+ register_lazy_module_prefix as _register_lazy_module_prefix,
72
+ register_lazy_module_methods as _register_lazy_module_methods,
73
+ LazyImporter,
74
+ LazyModuleRegistry,
75
+ )
76
+
77
+ logger = get_logger("xwlazy.facade")
78
+
79
+ # Global instances
80
+ _lazy_importer = LazyImporter()
81
+ _lazy_module_registry = LazyModuleRegistry()
82
+
83
+ # =============================================================================
84
+ # FACADE CLASS
85
+ # =============================================================================
86
+
87
+ class LazyModeFacade:
88
+ """
89
+ Facade class for managing lazy mode configuration and operations.
90
+
91
+ This class provides a unified interface to all lazy loading functionality.
92
+ """
93
+
94
+ def __init__(self):
95
+ self._enabled = False
96
+ self._strategy = "on_demand"
97
+ self._configs: Dict[str, Any] = {}
98
+
99
+ def enable(self, strategy: str = "on_demand", **kwargs) -> None:
100
+ """Enable lazy mode with specified strategy."""
101
+ self._enabled = True
102
+ self._strategy = strategy
103
+ self._configs.update(kwargs)
104
+ logger.info(f"Lazy mode enabled with strategy: {strategy}")
105
+
106
+ def disable(self) -> None:
107
+ """Disable lazy mode and cleanup resources."""
108
+ self._enabled = False
109
+ logger.info("Lazy mode disabled")
110
+
111
+ def is_enabled(self) -> bool:
112
+ """Check if lazy mode is currently enabled."""
113
+ return self._enabled
114
+
115
+ def get_stats(self) -> Dict[str, Any]:
116
+ """Get lazy mode performance statistics."""
117
+ return {
118
+ "enabled": self._enabled,
119
+ "strategy": self._strategy,
120
+ "configs": self._configs.copy(),
121
+ }
122
+
123
+
124
+ # Global facade instance
125
+ _lazy_facade = LazyModeFacade()
126
+
127
+ # =============================================================================
128
+ # FACADE FUNCTIONS
129
+ # =============================================================================
130
+
131
+ def enable_lazy_mode(strategy: str = "on_demand", **kwargs) -> None:
132
+ """Enable lazy mode with specified strategy."""
133
+ _lazy_facade.enable(strategy, **kwargs)
134
+
135
+
136
+ def disable_lazy_mode() -> None:
137
+ """Disable lazy mode and cleanup resources."""
138
+ _lazy_facade.disable()
139
+
140
+
141
+ def is_lazy_mode_enabled() -> bool:
142
+ """Check if lazy mode is currently enabled."""
143
+ return _lazy_facade.is_enabled()
144
+
145
+
146
+ def get_lazy_mode_stats() -> Dict[str, Any]:
147
+ """Get lazy mode performance statistics."""
148
+ return _lazy_facade.get_stats()
149
+
150
+
151
+ def configure_lazy_mode(package_name: str, config: LazyModeConfig) -> None:
152
+ """Configure lazy mode for a specific package."""
153
+ LazyInstallConfig.set_mode_config(package_name, config)
154
+ logger.info(f"Configured lazy mode for {package_name}")
155
+
156
+
157
+ def preload_modules(package_name: str, modules: List[str]) -> None:
158
+ """Preload specified modules for a package."""
159
+ for module_name in modules:
160
+ _lazy_importer.preload_module(module_name)
161
+ logger.info(f"Preloaded {len(modules)} modules for {package_name}")
162
+
163
+
164
+ def optimize_lazy_mode(package_name: str) -> None:
165
+ """Optimize lazy mode configuration for a package."""
166
+ _lazy_module_registry.preload_frequently_used()
167
+ logger.info(f"Optimization completed for {package_name}")
168
+
169
+
170
+ # =============================================================================
171
+ # PUBLIC API FUNCTIONS - Installation
172
+ # =============================================================================
173
+
174
+ def enable_lazy_install(package_name: str) -> None:
175
+ """Enable lazy installation for a package."""
176
+ LazyInstallConfig.set(package_name, True)
177
+
178
+
179
+ def disable_lazy_install(package_name: str) -> None:
180
+ """Disable lazy installation for a package."""
181
+ LazyInstallConfig.set(package_name, False)
182
+
183
+
184
+ def is_lazy_install_enabled(package_name: str) -> bool:
185
+ """Check if lazy installation is enabled for a package."""
186
+ return LazyInstallConfig.is_enabled(package_name)
187
+
188
+
189
+ def set_lazy_install_mode(package_name: str, mode: LazyInstallMode) -> None:
190
+ """Set lazy installation mode for a package."""
191
+ LazyInstallConfig.set_install_mode(package_name, mode)
192
+
193
+
194
+ def get_lazy_install_mode(package_name: str) -> LazyInstallMode:
195
+ """Get lazy installation mode for a package."""
196
+ return LazyInstallConfig.get_install_mode(package_name)
197
+
198
+
199
+ def install_missing_package(package_name: str, module_name: str, installer_package: str = 'default') -> bool:
200
+ """Install a missing package for a module."""
201
+ try:
202
+ installer = LazyInstallerRegistry.get_instance(installer_package)
203
+ if not installer.is_enabled():
204
+ logger.debug(f"Lazy installation disabled for {installer_package}")
205
+ return False
206
+ return installer.install_package(package_name, module_name)
207
+ except Exception as e:
208
+ logger.error(f"Failed to install package {package_name} for {installer_package}: {e}")
209
+ return False
210
+
211
+
212
+ def install_and_import(module_name: str, package_name: str = None, installer_package: str = 'default') -> Tuple[Optional[ModuleType], bool]:
213
+ """Install package and import module."""
214
+ try:
215
+ installer = LazyInstallerRegistry.get_instance(installer_package)
216
+ if not installer.is_enabled():
217
+ logger.debug(f"Lazy installation disabled for {installer_package}")
218
+ return None, False
219
+ return installer.install_and_import(module_name, package_name)
220
+ except Exception as e:
221
+ logger.error(f"Failed to install and import {module_name} for {installer_package}: {e}")
222
+ return None, False
223
+
224
+
225
+ def get_lazy_install_stats(package_name: str) -> Dict[str, Any]:
226
+ """Get installation statistics for a package."""
227
+ try:
228
+ installer = LazyInstallerRegistry.get_instance(package_name)
229
+ return installer.get_stats()
230
+ except Exception as e:
231
+ logger.error(f"Failed to get stats for {package_name}: {e}")
232
+ return {
233
+ 'enabled': False,
234
+ 'mode': 'unknown',
235
+ 'package_name': package_name,
236
+ 'installed_packages': [],
237
+ 'failed_packages': [],
238
+ 'total_installed': 0,
239
+ 'total_failed': 0,
240
+ }
241
+
242
+
243
+ def get_all_lazy_install_stats() -> Dict[str, Dict[str, Any]]:
244
+ """Get installation statistics for all packages."""
245
+ try:
246
+ all_instances = LazyInstallerRegistry.get_all_instances()
247
+ return {pkg_name: installer.get_stats() for pkg_name, installer in all_instances.items()}
248
+ except Exception as e:
249
+ logger.error(f"Failed to get all stats: {e}")
250
+ return {}
251
+
252
+
253
+ def lazy_import_with_install(module_name: str, package_name: str = None, installer_package: str = 'default') -> Tuple[Optional[ModuleType], bool]:
254
+ """Lazy import with automatic installation."""
255
+ try:
256
+ installer = LazyInstallerRegistry.get_instance(installer_package)
257
+ if not installer.is_enabled():
258
+ logger.debug(f"Lazy installation disabled for {installer_package}")
259
+ return None, False
260
+ return installer.install_and_import(module_name, package_name)
261
+ except Exception as e:
262
+ logger.error(f"Failed to lazy import with install {module_name} for {installer_package}: {e}")
263
+ return None, False
264
+
265
+
266
+ def xwimport(module_name: str, package_name: str = None, installer_package: str = 'default') -> Any:
267
+ """Simple lazy import with automatic installation."""
268
+ module, available = lazy_import_with_install(module_name, package_name, installer_package)
269
+ if not available:
270
+ raise ImportError(f"Module {module_name} is not available and could not be installed")
271
+ return module
272
+
273
+
274
+ # =============================================================================
275
+ # HOOK FUNCTIONS
276
+ # =============================================================================
277
+
278
+ def install_import_hook(package_name: str = 'default') -> None:
279
+ """Install performant import hook for automatic lazy installation."""
280
+ try:
281
+ _install_import_hook(package_name)
282
+ logger.info(f"Import hook installed for {package_name}")
283
+ except Exception as e:
284
+ logger.error(f"Failed to install import hook for {package_name}: {e}")
285
+ raise
286
+
287
+
288
+ def uninstall_import_hook(package_name: str = 'default') -> None:
289
+ """Uninstall import hook for a package."""
290
+ try:
291
+ _uninstall_import_hook(package_name)
292
+ logger.info(f"Import hook uninstalled for {package_name}")
293
+ except Exception as e:
294
+ logger.error(f"Failed to uninstall import hook for {package_name}: {e}")
295
+ raise
296
+
297
+
298
+ def is_import_hook_installed(package_name: str = 'default') -> bool:
299
+ """Check if import hook is installed for a package."""
300
+ try:
301
+ return _is_import_hook_installed(package_name)
302
+ except Exception:
303
+ return False
304
+
305
+
306
+ # =============================================================================
307
+ # LAZY LOADING FUNCTIONS
308
+ # =============================================================================
309
+
310
+ def enable_lazy_imports(mode: LazyLoadMode = LazyLoadMode.AUTO, package_name: Optional[str] = None) -> None:
311
+ """
312
+ Enable lazy imports.
313
+
314
+ This is a global setting that applies to all packages. The package_name
315
+ parameter is optional and used only for logging purposes.
316
+
317
+ Args:
318
+ mode: The lazy load mode to use (default: LazyLoadMode.AUTO)
319
+ package_name: Optional package name for logging purposes
320
+ """
321
+ try:
322
+ _lazy_importer.enable(mode)
323
+ # Also patch import_module to be lazy-aware (from archive)
324
+ from .module.importer_engine import _patch_import_module
325
+ _patch_import_module()
326
+ if package_name:
327
+ logger.info(f"Lazy imports enabled for {package_name} with mode {mode}")
328
+ else:
329
+ logger.info(f"Lazy imports enabled with mode {mode}")
330
+ except Exception as e:
331
+ if package_name:
332
+ logger.error(f"Failed to enable lazy imports for {package_name}: {e}")
333
+ else:
334
+ logger.error(f"Failed to enable lazy imports: {e}")
335
+ raise
336
+
337
+
338
+ def disable_lazy_imports(package_name: Optional[str] = None) -> None:
339
+ """
340
+ Disable lazy imports.
341
+
342
+ This is a global setting that applies to all packages. The package_name
343
+ parameter is optional and used only for logging purposes.
344
+
345
+ Args:
346
+ package_name: Optional package name for logging purposes
347
+ """
348
+ try:
349
+ _lazy_importer.disable()
350
+ # Also unpatch import_module (from archive)
351
+ from .module.importer_engine import _unpatch_import_module
352
+ _unpatch_import_module()
353
+ if package_name:
354
+ logger.info(f"Lazy imports disabled for {package_name}")
355
+ else:
356
+ logger.info("Lazy imports disabled")
357
+ except Exception as e:
358
+ if package_name:
359
+ logger.error(f"Failed to disable lazy imports for {package_name}: {e}")
360
+ else:
361
+ logger.error(f"Failed to disable lazy imports: {e}")
362
+ raise
363
+
364
+
365
+ def is_lazy_import_enabled(package_name: Optional[str] = None) -> bool:
366
+ """
367
+ Check if lazy imports are enabled.
368
+
369
+ This checks a global setting that applies to all packages. The package_name
370
+ parameter is optional and used only for logging purposes.
371
+
372
+ Args:
373
+ package_name: Optional package name for logging purposes
374
+
375
+ Returns:
376
+ True if lazy imports are enabled globally
377
+ """
378
+ try:
379
+ return _lazy_importer.is_enabled()
380
+ except (AttributeError, RuntimeError) as e:
381
+ logger.debug(f"Error checking lazy import status: {e}")
382
+ return False
383
+ except Exception as e:
384
+ # Unexpected errors - log but return False for safety
385
+ logger.warning(f"Unexpected error checking lazy import status: {e}")
386
+ return False
387
+
388
+
389
+ def lazy_import(module_name: str, package_name: str = None) -> Optional[ModuleType]:
390
+ """Lazy import a module."""
391
+ try:
392
+ return _lazy_importer.import_module(module_name, package_name)
393
+ except Exception as e:
394
+ logger.error(f"Failed to lazy import {module_name}: {e}")
395
+ return None
396
+
397
+
398
+ def register_lazy_module(module_name: str, package_name: str = None, module_path: str = None) -> None:
399
+ """Register a lazy module loader."""
400
+ try:
401
+ if module_path is None:
402
+ module_path = module_name
403
+ _lazy_importer.register_lazy_module(module_name, module_path)
404
+ # Also register in global registry (from archive)
405
+ _lazy_module_registry.register_module(module_name, module_path)
406
+ logger.info(f"Lazy module registered: {module_name}")
407
+ except Exception as e:
408
+ logger.error(f"Failed to register lazy module {module_name}: {e}")
409
+ raise
410
+
411
+
412
+ def preload_module(module_name: str, package_name: str = None) -> None:
413
+ """Preload a lazy module."""
414
+ try:
415
+ success = _lazy_importer.preload_module(module_name)
416
+ if success:
417
+ logger.info(f"Preload completed: {module_name}")
418
+ else:
419
+ logger.warning(f"Preload failed for {module_name}")
420
+ except Exception as e:
421
+ logger.error(f"Failed to preload module {module_name}: {e}")
422
+ raise
423
+
424
+
425
+ def get_lazy_module(module_name: str, package_name: str = None) -> Optional[ModuleType]:
426
+ """Get a lazy module if loaded."""
427
+ # Check if module is already loaded in importer
428
+ stats = _lazy_importer.get_stats()
429
+ if module_name in stats.get('loaded_modules', []):
430
+ # Module is loaded, return it via import (safe since it's cached)
431
+ try:
432
+ import importlib
433
+ return importlib.import_module(module_name)
434
+ except ImportError:
435
+ pass
436
+
437
+ # Fallback to registry
438
+ try:
439
+ loader = _lazy_module_registry.get_module(module_name)
440
+ if loader.is_loaded():
441
+ return loader.load_module()
442
+ except KeyError:
443
+ pass
444
+
445
+ # Check sys.modules as final fallback
446
+ import sys
447
+ return sys.modules.get(module_name)
448
+
449
+
450
+ def get_loading_stats(package_name: str) -> Dict[str, Any]:
451
+ """Get loading statistics for a package."""
452
+ try:
453
+ return _lazy_module_registry.get_stats()
454
+ except Exception as e:
455
+ logger.error(f"Failed to get loading stats for {package_name}: {e}")
456
+ return {
457
+ 'total_registered': 0,
458
+ 'loaded_count': 0,
459
+ 'unloaded_count': 0,
460
+ 'access_counts': {},
461
+ 'load_times': {},
462
+ }
463
+
464
+
465
+ def preload_frequently_used(package_name: str) -> None:
466
+ """Preload frequently used modules for a package."""
467
+ try:
468
+ _lazy_module_registry.preload_frequently_used()
469
+ logger.info(f"Preload frequently used completed for {package_name}")
470
+ except Exception as e:
471
+ logger.error(f"Failed to preload frequently used for {package_name}: {e}")
472
+
473
+
474
+ def get_lazy_import_stats(package_name: str) -> Dict[str, Any]:
475
+ """Get lazy import statistics for a package."""
476
+ try:
477
+ return _lazy_importer.get_stats()
478
+ except Exception as e:
479
+ logger.error(f"Failed to get lazy import stats for {package_name}: {e}")
480
+ return {
481
+ 'enabled': False,
482
+ 'registered_modules': [],
483
+ 'loaded_modules': [],
484
+ 'access_counts': {},
485
+ 'total_registered': 0,
486
+ 'total_loaded': 0,
487
+ }
488
+
489
+
490
+ # =============================================================================
491
+ # CONFIGURATION FUNCTIONS
492
+ # =============================================================================
493
+
494
+ def config_package_lazy_install_enabled(
495
+ package_name: str,
496
+ enabled: bool = None,
497
+ mode: str = "auto",
498
+ install_hook: bool = True,
499
+ load_mode: Optional[LazyLoadMode] = None,
500
+ install_mode: Optional[LazyInstallMode] = None,
501
+ mode_config: Optional[LazyModeConfig] = None,
502
+ # Strategy parameters
503
+ execution_strategy: Optional[Any] = None,
504
+ timing_strategy: Optional[Any] = None,
505
+ discovery_strategy: Optional[Any] = None,
506
+ policy_strategy: Optional[Any] = None,
507
+ mapping_strategy: Optional[Any] = None,
508
+ ) -> bool:
509
+ """
510
+ Configure lazy installation for a package.
511
+
512
+ Args:
513
+ package_name: Package name to configure
514
+ enabled: Whether lazy installation is enabled (None = auto-detect)
515
+ mode: Installation mode string (e.g., "smart", "full", "clean")
516
+ install_hook: Whether to install the import hook
517
+ load_mode: Optional explicit load mode
518
+ install_mode: Optional explicit install mode
519
+ mode_config: Optional full mode configuration
520
+ execution_strategy: Optional custom execution strategy instance
521
+ timing_strategy: Optional custom timing strategy instance
522
+ discovery_strategy: Optional custom discovery strategy instance
523
+ policy_strategy: Optional custom policy strategy instance
524
+ mapping_strategy: Optional custom mapping strategy instance
525
+
526
+ Returns:
527
+ True if enabled, False otherwise
528
+ """
529
+ try:
530
+ # Store strategies if provided
531
+ from .package.services.strategy_registry import StrategyRegistry
532
+ if execution_strategy is not None:
533
+ StrategyRegistry.set_package_strategy(package_name, 'execution', execution_strategy)
534
+ if timing_strategy is not None:
535
+ StrategyRegistry.set_package_strategy(package_name, 'timing', timing_strategy)
536
+ if discovery_strategy is not None:
537
+ StrategyRegistry.set_package_strategy(package_name, 'discovery', discovery_strategy)
538
+ if policy_strategy is not None:
539
+ StrategyRegistry.set_package_strategy(package_name, 'policy', policy_strategy)
540
+ if mapping_strategy is not None:
541
+ StrategyRegistry.set_package_strategy(package_name, 'mapping', mapping_strategy)
542
+
543
+ manual_override = enabled is not None
544
+ if enabled is None:
545
+ enabled = _detect_lazy_installation(package_name)
546
+
547
+ # Check meta info for mode override
548
+ if mode == "auto" and enabled:
549
+ meta_mode = _detect_meta_info_mode(package_name)
550
+ if meta_mode:
551
+ mode = meta_mode
552
+
553
+ # Resolve preset mode if provided
554
+ if load_mode is None and install_mode is None and mode_config is None:
555
+ preset = get_preset_mode(mode)
556
+ if preset:
557
+ mode_config = preset
558
+
559
+ LazyInstallConfig.set(
560
+ package_name,
561
+ enabled,
562
+ mode,
563
+ install_hook=install_hook,
564
+ manual=manual_override,
565
+ load_mode=load_mode,
566
+ install_mode=install_mode,
567
+ mode_config=mode_config,
568
+ )
569
+
570
+ # Install hook if requested and enabled
571
+ if install_hook and enabled:
572
+ try:
573
+ install_import_hook(package_name)
574
+ except Exception as hook_error:
575
+ logger.warning(f"Failed to install import hook for {package_name}: {hook_error}")
576
+
577
+ result = LazyInstallConfig.is_enabled(package_name)
578
+ logger.info(f"Configured lazy installation for {package_name}: enabled={result}, mode={mode}")
579
+ return result
580
+ except Exception as e:
581
+ logger.error(f"Failed to configure lazy installation for {package_name}: {e}")
582
+ raise
583
+
584
+
585
+ def config_module_lazy_load_enabled(
586
+ package_name: str,
587
+ enabled: bool = True,
588
+ load_mode: Optional[LazyLoadMode] = None,
589
+ # Strategy parameters
590
+ helper_strategy: Optional[Any] = None,
591
+ manager_strategy: Optional[Any] = None,
592
+ caching_strategy: Optional[Any] = None,
593
+ ) -> bool:
594
+ """
595
+ Configure lazy loading for modules in a package.
596
+
597
+ Args:
598
+ package_name: Package name to configure
599
+ enabled: Whether lazy loading is enabled (default: True)
600
+ load_mode: Optional explicit load mode
601
+ helper_strategy: Optional custom helper strategy instance
602
+ manager_strategy: Optional custom manager strategy instance
603
+ caching_strategy: Optional custom caching strategy instance
604
+
605
+ Returns:
606
+ True if enabled, False otherwise
607
+ """
608
+ try:
609
+ # Store strategies if provided
610
+ from .package.services.strategy_registry import StrategyRegistry
611
+ if helper_strategy is not None:
612
+ StrategyRegistry.set_module_strategy(package_name, 'helper', helper_strategy)
613
+ if manager_strategy is not None:
614
+ StrategyRegistry.set_module_strategy(package_name, 'manager', manager_strategy)
615
+ if caching_strategy is not None:
616
+ StrategyRegistry.set_module_strategy(package_name, 'caching', caching_strategy)
617
+
618
+ # Enable lazy imports if requested
619
+ if enabled:
620
+ if load_mode is None:
621
+ load_mode = LazyLoadMode.AUTO
622
+ enable_lazy_imports(load_mode, package_name=package_name)
623
+ else:
624
+ disable_lazy_imports(package_name=package_name)
625
+
626
+ logger.info(f"Configured lazy loading for {package_name}: enabled={enabled}, load_mode={load_mode}")
627
+ return enabled
628
+ except Exception as e:
629
+ logger.error(f"Failed to configure lazy loading for {package_name}: {e}")
630
+ raise
631
+
632
+
633
+ def sync_manifest_configuration(package_name: str) -> None:
634
+ """
635
+ Sync configuration from manifest for a specific package.
636
+
637
+ This syncs all manifest settings including:
638
+ - Dependencies
639
+ - Watched prefixes
640
+ - Class wrap prefixes (registered as package class hints)
641
+ - Async configuration
642
+ """
643
+ try:
644
+ from .module.importer_engine import _set_package_class_hints
645
+
646
+ from .package.services.manifest import _normalize_package_name, get_manifest_loader as _get_manifest_loader
647
+ package_key = _normalize_package_name(package_name)
648
+
649
+ # Get the loader instance - this ensures we use the same instance throughout
650
+ # This is critical for tests that patch get_manifest_loader
651
+ loader = _get_manifest_loader()
652
+
653
+ # Sync manifest configuration using the loader instance directly
654
+ # This ensures we use the same loader instance that we'll use to get the manifest
655
+ loader.sync_manifest_configuration(package_name)
656
+
657
+ # Register class wrap prefixes from manifest as package class hints
658
+ # After sync, the manifest cache is cleared, so get_manifest will reload it
659
+ manifest = loader.get_manifest(package_key)
660
+ if manifest and manifest.class_wrap_prefixes:
661
+ _set_package_class_hints(package_key, manifest.class_wrap_prefixes)
662
+
663
+ logger.info(f"Manifest configuration synced for {package_name}")
664
+ except Exception as e:
665
+ logger.error(f"Failed to sync manifest configuration for {package_name}: {e}")
666
+ raise
667
+
668
+
669
+ def refresh_lazy_manifests() -> None:
670
+ """Refresh all lazy manifest caches."""
671
+ try:
672
+ refresh_manifest_cache()
673
+ logger.info("Refreshed all lazy manifest caches")
674
+ except Exception as e:
675
+ logger.error(f"Failed to refresh lazy manifest caches: {e}")
676
+ raise
677
+
678
+
679
+ # =============================================================================
680
+ # SECURITY & POLICY FUNCTIONS
681
+ # =============================================================================
682
+
683
+ def set_package_allow_list(package_name: str, allowed_packages: List[str]) -> None:
684
+ """Set allow list for a package."""
685
+ try:
686
+ LazyInstallPolicy.set_allow_list(package_name, allowed_packages)
687
+ logger.debug(f"Set allow list for {package_name}: {allowed_packages}")
688
+ except Exception as e:
689
+ logger.error(f"Failed to set allow list for {package_name}: {e}")
690
+ raise
691
+
692
+
693
+ def set_package_deny_list(package_name: str, denied_packages: List[str]) -> None:
694
+ """Set deny list for a package."""
695
+ try:
696
+ LazyInstallPolicy.set_deny_list(package_name, denied_packages)
697
+ logger.debug(f"Set deny list for {package_name}: {denied_packages}")
698
+ except Exception as e:
699
+ logger.error(f"Failed to set deny list for {package_name}: {e}")
700
+ raise
701
+
702
+
703
+ def add_to_package_allow_list(package_name: str, allowed_package: str) -> None:
704
+ """Add single package to allow list."""
705
+ try:
706
+ LazyInstallPolicy.add_to_allow_list(package_name, allowed_package)
707
+ logger.debug(f"Added {allowed_package} to allow list for {package_name}")
708
+ except Exception as e:
709
+ logger.error(f"Failed to add {allowed_package} to allow list for {package_name}: {e}")
710
+ raise
711
+
712
+
713
+ def add_to_package_deny_list(package_name: str, denied_package: str) -> None:
714
+ """Add single package to deny list."""
715
+ try:
716
+ LazyInstallPolicy.add_to_deny_list(package_name, denied_package)
717
+ logger.debug(f"Added {denied_package} to deny list for {package_name}")
718
+ except Exception as e:
719
+ logger.error(f"Failed to add {denied_package} to deny list for {package_name}: {e}")
720
+ raise
721
+
722
+
723
+ def set_package_index_url(package_name: str, index_url: str) -> None:
724
+ """Set package index URL for a package."""
725
+ try:
726
+ LazyInstallPolicy.set_index_url(package_name, index_url)
727
+ logger.debug(f"Set index URL for {package_name}: {index_url}")
728
+ except Exception as e:
729
+ logger.error(f"Failed to set index URL for {package_name}: {e}")
730
+ raise
731
+
732
+
733
+ def set_package_extra_index_urls(package_name: str, extra_index_urls: List[str]) -> None:
734
+ """Set extra index URLs for a package."""
735
+ try:
736
+ LazyInstallPolicy.set_extra_index_urls(package_name, extra_index_urls)
737
+ logger.debug(f"Set extra index URLs for {package_name}: {extra_index_urls}")
738
+ except Exception as e:
739
+ logger.error(f"Failed to set extra index URLs for {package_name}: {e}")
740
+ raise
741
+
742
+
743
+ def add_package_trusted_host(package_name: str, host: str) -> None:
744
+ """Add trusted host for a package."""
745
+ try:
746
+ LazyInstallPolicy.add_trusted_host(package_name, host)
747
+ logger.debug(f"Added trusted host {host} for {package_name}")
748
+ except Exception as e:
749
+ logger.error(f"Failed to add trusted host {host} for {package_name}: {e}")
750
+ raise
751
+
752
+
753
+ def set_package_lockfile(package_name: str, lockfile_path: str) -> None:
754
+ """Set lockfile path for a package."""
755
+ try:
756
+ LazyInstallPolicy.set_lockfile_path(package_name, lockfile_path)
757
+ logger.debug(f"Set lockfile path for {package_name}: {lockfile_path}")
758
+ except Exception as e:
759
+ logger.error(f"Failed to set lockfile path for {package_name}: {e}")
760
+ raise
761
+
762
+
763
+ def generate_package_sbom(package_name: str, output_path: Optional[str] = None) -> Dict[str, Any]:
764
+ """Generate SBOM for a package."""
765
+ try:
766
+ installer = LazyInstallerRegistry.get_instance(package_name)
767
+ sbom = installer.generate_sbom()
768
+ if output_path:
769
+ installer.export_sbom(output_path)
770
+ return sbom
771
+ except Exception as e:
772
+ logger.error(f"Failed to generate SBOM for {package_name}: {e}")
773
+ return {
774
+ "metadata": {
775
+ "format": "xwlazy-sbom",
776
+ "version": "1.0",
777
+ "error": str(e)
778
+ },
779
+ "packages": []
780
+ }
781
+
782
+
783
+ def check_externally_managed_environment(package_name: str = 'default') -> bool:
784
+ """Check if environment is externally managed."""
785
+ try:
786
+ return _is_externally_managed()
787
+ except Exception as e:
788
+ logger.error(f"Failed to check externally managed environment: {e}")
789
+ return False
790
+
791
+
792
+ def register_lazy_module_prefix(prefix: str) -> None:
793
+ """Register a module prefix mapping."""
794
+ try:
795
+ _register_lazy_module_prefix(prefix)
796
+ logger.debug(f"Registered lazy module prefix: {prefix}")
797
+ except Exception as e:
798
+ logger.error(f"Failed to register lazy module prefix {prefix}: {e}")
799
+ raise
800
+
801
+
802
+ def register_lazy_module_methods(prefix: str, methods: Tuple[str, ...]) -> None:
803
+ """Register methods for a lazy module."""
804
+ try:
805
+ _register_lazy_module_methods(prefix, methods)
806
+ logger.debug(f"Registered lazy module methods for prefix {prefix}: {methods}")
807
+ except Exception as e:
808
+ logger.error(f"Failed to register lazy module methods for {prefix}: {e}")
809
+ raise
810
+
811
+
812
+ # =============================================================================
813
+ # MODULE REGISTRATION DOMAIN - Internal Utilities
814
+ # =============================================================================
815
+
816
+ # Note: Internal utility functions are available from hooks.finder module
817
+ # They are used internally by the lazy loading system and don't need facade wrappers
818
+
819
+
820
+ # =============================================================================
821
+ # KEYWORD-BASED DETECTION FUNCTIONS
822
+ # =============================================================================
823
+
824
+ def enable_keyword_detection(enabled: bool = True, keyword: Optional[str] = None, package_name: Optional[str] = None) -> None:
825
+ """
826
+ Enable keyword-based package detection.
827
+
828
+ This is a global setting that applies to all packages. The package_name
829
+ parameter is optional and used only for logging purposes.
830
+
831
+ Args:
832
+ enabled: Whether to enable keyword detection (default: True)
833
+ keyword: Custom keyword to check (default: "xwlazy-enabled")
834
+ package_name: Optional package name for logging purposes
835
+ """
836
+ try:
837
+ _enable_keyword_detection(enabled, keyword)
838
+ if package_name:
839
+ logger.info(f"Keyword detection {'enabled' if enabled else 'disabled'} for {package_name}")
840
+ else:
841
+ logger.info(f"Keyword detection {'enabled' if enabled else 'disabled'}")
842
+ except Exception as e:
843
+ if package_name:
844
+ logger.error(f"Failed to enable keyword detection for {package_name}: {e}")
845
+ else:
846
+ logger.error(f"Failed to enable keyword detection: {e}")
847
+ raise
848
+
849
+
850
+ def is_keyword_detection_enabled(package_name: Optional[str] = None) -> bool:
851
+ """
852
+ Check if keyword detection is enabled.
853
+
854
+ This checks a global setting that applies to all packages. The package_name
855
+ parameter is optional and used only for logging purposes.
856
+
857
+ Args:
858
+ package_name: Optional package name for logging purposes
859
+
860
+ Returns:
861
+ True if keyword detection is enabled globally
862
+ """
863
+ try:
864
+ return _is_keyword_detection_enabled()
865
+ except (AttributeError, RuntimeError) as e:
866
+ logger.debug(f"Error checking keyword detection status: {e}")
867
+ return False
868
+ except Exception as e:
869
+ # Unexpected errors - log but return False for safety
870
+ logger.warning(f"Unexpected error checking keyword detection status: {e}")
871
+ return False
872
+
873
+
874
+ def get_keyword_detection_keyword(package_name: Optional[str] = None) -> Optional[str]:
875
+ """
876
+ Get keyword used for detection.
877
+
878
+ This returns the global keyword setting. The package_name parameter is
879
+ optional and used only for logging purposes.
880
+
881
+ Args:
882
+ package_name: Optional package name for logging purposes
883
+
884
+ Returns:
885
+ The keyword being checked for auto-detection
886
+ """
887
+ try:
888
+ return _get_keyword_detection_keyword()
889
+ except Exception as e:
890
+ logger.error(f"Failed to get keyword detection keyword: {e}")
891
+ return None
892
+
893
+
894
+ def check_package_keywords(package_name: Optional[str] = None, keywords: Optional[List[str]] = None) -> bool:
895
+ """
896
+ Check if a package (or any package) has the specified keyword in its metadata.
897
+
898
+ Args:
899
+ package_name: The package name to check (or None to check all packages)
900
+ keywords: Optional list of keywords to check (uses first keyword if provided)
901
+
902
+ Returns:
903
+ True if the keyword is found in the package's metadata
904
+ """
905
+ try:
906
+ keyword = keywords[0] if keywords else None
907
+ return _check_package_keywords(package_name, keyword)
908
+ except Exception as e:
909
+ if package_name:
910
+ logger.error(f"Failed to check package keywords for {package_name}: {e}")
911
+ else:
912
+ logger.error(f"Failed to check package keywords: {e}")
913
+ return False
914
+
915
+
916
+ # =============================================================================
917
+ # DISCOVERY FUNCTIONS
918
+ # =============================================================================
919
+
920
+ def get_lazy_discovery(package_name: str = 'default') -> Optional[APackageHelper]:
921
+ """Get discovery instance for a package."""
922
+ try:
923
+ return _get_lazy_discovery()
924
+ except Exception as e:
925
+ logger.error(f"Failed to get discovery instance for {package_name}: {e}")
926
+ return None
927
+
928
+
929
+ def discover_dependencies(package_name: str = 'default') -> Dict[str, str]:
930
+ """Discover dependencies for a package."""
931
+ try:
932
+ discovery = _get_lazy_discovery()
933
+ if discovery:
934
+ return discovery.discover_all_dependencies()
935
+ except Exception as e:
936
+ logger.error(f"Failed to discover dependencies for {package_name}: {e}")
937
+ return {}
938
+
939
+
940
+ def export_dependency_mappings(package_name: str = 'default', output_path: Optional[str] = None) -> None:
941
+ """Export dependency mappings to file."""
942
+ try:
943
+ discovery = _get_lazy_discovery()
944
+ if discovery:
945
+ if output_path:
946
+ discovery.export_to_json(output_path)
947
+ else:
948
+ # Default output path
949
+ from pathlib import Path
950
+ output_path = Path.cwd() / f"{package_name}_dependencies.json"
951
+ discovery.export_to_json(str(output_path))
952
+ logger.info(f"Dependency mappings exported for {package_name} to {output_path}")
953
+ else:
954
+ logger.warning(f"No discovery instance available for {package_name}")
955
+ except Exception as e:
956
+ logger.error(f"Failed to export dependency mappings for {package_name}: {e}")
957
+ raise
958
+
959
+
960
+ # =============================================================================
961
+ # PUBLIC API EXPORTS
962
+ # =============================================================================
963
+
964
+ __all__ = [
965
+ # Facade class
966
+ 'LazyModeFacade',
967
+ # Facade functions
968
+ 'enable_lazy_mode',
969
+ 'disable_lazy_mode',
970
+ 'is_lazy_mode_enabled',
971
+ 'get_lazy_mode_stats',
972
+ 'configure_lazy_mode',
973
+ 'preload_modules',
974
+ 'optimize_lazy_mode',
975
+ # Public API functions
976
+ 'enable_lazy_install',
977
+ 'disable_lazy_install',
978
+ 'is_lazy_install_enabled',
979
+ 'set_lazy_install_mode',
980
+ 'get_lazy_install_mode',
981
+ 'install_missing_package',
982
+ 'install_and_import',
983
+ 'get_lazy_install_stats',
984
+ 'get_all_lazy_install_stats',
985
+ 'lazy_import_with_install',
986
+ 'xwimport',
987
+ # Hook functions
988
+ 'install_import_hook',
989
+ 'uninstall_import_hook',
990
+ 'is_import_hook_installed',
991
+ # Lazy loading functions
992
+ 'enable_lazy_imports',
993
+ 'disable_lazy_imports',
994
+ 'is_lazy_import_enabled',
995
+ 'lazy_import',
996
+ 'register_lazy_module',
997
+ 'preload_module',
998
+ 'get_lazy_module',
999
+ 'get_loading_stats',
1000
+ 'preload_frequently_used',
1001
+ 'get_lazy_import_stats',
1002
+ # Configuration
1003
+ 'config_package_lazy_install_enabled',
1004
+ 'config_module_lazy_load_enabled',
1005
+ 'sync_manifest_configuration',
1006
+ 'refresh_lazy_manifests',
1007
+ # Security & Policy
1008
+ 'set_package_allow_list',
1009
+ 'set_package_deny_list',
1010
+ 'add_to_package_allow_list',
1011
+ 'add_to_package_deny_list',
1012
+ 'set_package_index_url',
1013
+ 'set_package_extra_index_urls',
1014
+ 'add_package_trusted_host',
1015
+ 'set_package_lockfile',
1016
+ 'generate_package_sbom',
1017
+ 'check_externally_managed_environment',
1018
+ 'register_lazy_module_prefix',
1019
+ 'register_lazy_module_methods',
1020
+ # Keyword-based detection
1021
+ 'enable_keyword_detection',
1022
+ 'is_keyword_detection_enabled',
1023
+ 'get_keyword_detection_keyword',
1024
+ 'check_package_keywords',
1025
+ # Discovery functions
1026
+ 'get_lazy_discovery',
1027
+ 'discover_dependencies',
1028
+ 'export_dependency_mappings',
1029
+ # Helper classes
1030
+ 'XWPackageHelper',
1031
+ 'XWModuleHelper',
1032
+ ]
1033
+
1034
+
1035
+ # =============================================================================
1036
+ # CONCRETE HELPER IMPLEMENTATIONS (Simple API Pattern)
1037
+ # =============================================================================
1038
+
1039
+ # XWPackageHelper moved to package/facade.py
1040
+ # This is now just an alias (defined above)
1041
+ # Removed duplicate class definition - use the one from package/facade.py
1042
+ # XWModuleHelper moved to module/facade.py
1043
+ # This is now just an alias (defined above)
1044
+ # Removed duplicate class definition - use the one from module/facade.py
1045
+
1046
+
1047
+ # Global helper instances
1048
+ _package_helper = XWPackageHelper()
1049
+ _module_helper = XWModuleHelper()