exonware-xwsystem 0.0.1.407__py3-none-any.whl → 0.0.1.408__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 (244) hide show
  1. exonware/__init__.py +21 -12
  2. exonware/conf.py +89 -149
  3. exonware/xwsystem/__init__.py +18 -159
  4. exonware/xwsystem/caching/__init__.py +1 -1
  5. exonware/xwsystem/caching/base.py +1 -1
  6. exonware/xwsystem/caching/bloom_cache.py +1 -1
  7. exonware/xwsystem/caching/cache_manager.py +1 -1
  8. exonware/xwsystem/caching/conditional.py +1 -1
  9. exonware/xwsystem/caching/contracts.py +1 -1
  10. exonware/xwsystem/caching/decorators.py +1 -1
  11. exonware/xwsystem/caching/defs.py +1 -1
  12. exonware/xwsystem/caching/disk_cache.py +1 -1
  13. exonware/xwsystem/caching/distributed.py +1 -1
  14. exonware/xwsystem/caching/errors.py +1 -1
  15. exonware/xwsystem/caching/events.py +1 -1
  16. exonware/xwsystem/caching/eviction_strategies.py +1 -1
  17. exonware/xwsystem/caching/fluent.py +1 -1
  18. exonware/xwsystem/caching/integrity.py +1 -1
  19. exonware/xwsystem/caching/lfu_cache.py +1 -1
  20. exonware/xwsystem/caching/lfu_optimized.py +1 -1
  21. exonware/xwsystem/caching/lru_cache.py +1 -1
  22. exonware/xwsystem/caching/memory_bounded.py +1 -1
  23. exonware/xwsystem/caching/metrics_exporter.py +1 -1
  24. exonware/xwsystem/caching/observable_cache.py +1 -1
  25. exonware/xwsystem/caching/pluggable_cache.py +1 -1
  26. exonware/xwsystem/caching/rate_limiter.py +1 -1
  27. exonware/xwsystem/caching/read_through.py +1 -1
  28. exonware/xwsystem/caching/secure_cache.py +1 -1
  29. exonware/xwsystem/caching/serializable.py +1 -1
  30. exonware/xwsystem/caching/stats.py +1 -1
  31. exonware/xwsystem/caching/tagging.py +1 -1
  32. exonware/xwsystem/caching/ttl_cache.py +1 -1
  33. exonware/xwsystem/caching/two_tier_cache.py +1 -1
  34. exonware/xwsystem/caching/utils.py +1 -1
  35. exonware/xwsystem/caching/validation.py +1 -1
  36. exonware/xwsystem/caching/warming.py +1 -1
  37. exonware/xwsystem/caching/write_behind.py +1 -1
  38. exonware/xwsystem/cli/__init__.py +1 -1
  39. exonware/xwsystem/cli/args.py +1 -1
  40. exonware/xwsystem/cli/base.py +1 -1
  41. exonware/xwsystem/cli/colors.py +1 -1
  42. exonware/xwsystem/cli/console.py +1 -1
  43. exonware/xwsystem/cli/contracts.py +1 -1
  44. exonware/xwsystem/cli/defs.py +1 -1
  45. exonware/xwsystem/cli/errors.py +1 -1
  46. exonware/xwsystem/cli/progress.py +1 -1
  47. exonware/xwsystem/cli/prompts.py +1 -1
  48. exonware/xwsystem/cli/tables.py +1 -1
  49. exonware/xwsystem/conf.py +1 -21
  50. exonware/xwsystem/config/__init__.py +1 -1
  51. exonware/xwsystem/config/base.py +1 -1
  52. exonware/xwsystem/config/contracts.py +1 -1
  53. exonware/xwsystem/config/defaults.py +1 -1
  54. exonware/xwsystem/config/defs.py +1 -1
  55. exonware/xwsystem/config/errors.py +1 -1
  56. exonware/xwsystem/config/logging.py +1 -1
  57. exonware/xwsystem/config/logging_setup.py +1 -1
  58. exonware/xwsystem/config/performance.py +1 -1
  59. exonware/xwsystem/http/__init__.py +1 -1
  60. exonware/xwsystem/http/advanced_client.py +5 -1
  61. exonware/xwsystem/http/base.py +1 -1
  62. exonware/xwsystem/http/client.py +5 -1
  63. exonware/xwsystem/http/contracts.py +1 -1
  64. exonware/xwsystem/http/defs.py +1 -1
  65. exonware/xwsystem/http/errors.py +1 -1
  66. exonware/xwsystem/io/__init__.py +1 -1
  67. exonware/xwsystem/io/archive/__init__.py +1 -1
  68. exonware/xwsystem/io/archive/archive.py +1 -1
  69. exonware/xwsystem/io/archive/archive_files.py +1 -1
  70. exonware/xwsystem/io/archive/archivers.py +1 -1
  71. exonware/xwsystem/io/archive/base.py +1 -1
  72. exonware/xwsystem/io/archive/codec_integration.py +1 -1
  73. exonware/xwsystem/io/archive/compression.py +1 -1
  74. exonware/xwsystem/io/archive/formats/__init__.py +1 -1
  75. exonware/xwsystem/io/archive/formats/brotli_format.py +1 -1
  76. exonware/xwsystem/io/archive/formats/lz4_format.py +1 -1
  77. exonware/xwsystem/io/archive/formats/rar.py +1 -1
  78. exonware/xwsystem/io/archive/formats/sevenzip.py +1 -1
  79. exonware/xwsystem/io/archive/formats/squashfs_format.py +1 -1
  80. exonware/xwsystem/io/archive/formats/tar.py +1 -1
  81. exonware/xwsystem/io/archive/formats/wim_format.py +1 -1
  82. exonware/xwsystem/io/archive/formats/zip.py +1 -1
  83. exonware/xwsystem/io/archive/formats/zpaq_format.py +1 -1
  84. exonware/xwsystem/io/archive/formats/zstandard.py +1 -1
  85. exonware/xwsystem/io/base.py +1 -1
  86. exonware/xwsystem/io/codec/__init__.py +1 -1
  87. exonware/xwsystem/io/codec/base.py +1 -1
  88. exonware/xwsystem/io/codec/contracts.py +1 -1
  89. exonware/xwsystem/io/codec/registry.py +1 -1
  90. exonware/xwsystem/io/common/__init__.py +1 -1
  91. exonware/xwsystem/io/common/base.py +1 -1
  92. exonware/xwsystem/io/common/lock.py +1 -1
  93. exonware/xwsystem/io/common/watcher.py +1 -1
  94. exonware/xwsystem/io/contracts.py +1 -1
  95. exonware/xwsystem/io/defs.py +1 -1
  96. exonware/xwsystem/io/errors.py +1 -1
  97. exonware/xwsystem/io/facade.py +1 -1
  98. exonware/xwsystem/io/file/__init__.py +1 -1
  99. exonware/xwsystem/io/file/base.py +1 -1
  100. exonware/xwsystem/io/file/conversion.py +1 -1
  101. exonware/xwsystem/io/file/file.py +1 -1
  102. exonware/xwsystem/io/file/paged_source.py +1 -1
  103. exonware/xwsystem/io/file/paging/__init__.py +1 -1
  104. exonware/xwsystem/io/file/paging/byte_paging.py +1 -1
  105. exonware/xwsystem/io/file/paging/line_paging.py +1 -1
  106. exonware/xwsystem/io/file/paging/record_paging.py +1 -1
  107. exonware/xwsystem/io/file/paging/registry.py +1 -1
  108. exonware/xwsystem/io/file/source.py +1 -1
  109. exonware/xwsystem/io/filesystem/__init__.py +1 -1
  110. exonware/xwsystem/io/filesystem/base.py +1 -1
  111. exonware/xwsystem/io/filesystem/local.py +1 -1
  112. exonware/xwsystem/io/folder/__init__.py +1 -1
  113. exonware/xwsystem/io/folder/base.py +1 -1
  114. exonware/xwsystem/io/folder/folder.py +1 -1
  115. exonware/xwsystem/io/serialization/__init__.py +1 -1
  116. exonware/xwsystem/io/serialization/auto_serializer.py +1 -1
  117. exonware/xwsystem/io/serialization/base.py +2 -2
  118. exonware/xwsystem/io/serialization/contracts.py +1 -1
  119. exonware/xwsystem/io/serialization/defs.py +1 -1
  120. exonware/xwsystem/io/serialization/errors.py +1 -1
  121. exonware/xwsystem/io/serialization/flyweight.py +1 -1
  122. exonware/xwsystem/io/serialization/format_detector.py +1 -1
  123. exonware/xwsystem/io/serialization/formats/__init__.py +1 -1
  124. exonware/xwsystem/io/serialization/formats/binary/bson.py +1 -1
  125. exonware/xwsystem/io/serialization/formats/binary/cbor.py +1 -1
  126. exonware/xwsystem/io/serialization/formats/binary/marshal.py +1 -1
  127. exonware/xwsystem/io/serialization/formats/binary/msgpack.py +1 -1
  128. exonware/xwsystem/io/serialization/formats/binary/pickle.py +1 -1
  129. exonware/xwsystem/io/serialization/formats/binary/plistlib.py +1 -1
  130. exonware/xwsystem/io/serialization/formats/database/dbm.py +1 -1
  131. exonware/xwsystem/io/serialization/formats/database/shelve.py +1 -1
  132. exonware/xwsystem/io/serialization/formats/database/sqlite3.py +1 -1
  133. exonware/xwsystem/io/serialization/formats/text/configparser.py +1 -1
  134. exonware/xwsystem/io/serialization/formats/text/csv.py +1 -1
  135. exonware/xwsystem/io/serialization/formats/text/formdata.py +1 -1
  136. exonware/xwsystem/io/serialization/formats/text/json.py +1 -1
  137. exonware/xwsystem/io/serialization/formats/text/json5.py +1 -1
  138. exonware/xwsystem/io/serialization/formats/text/jsonlines.py +1 -1
  139. exonware/xwsystem/io/serialization/formats/text/multipart.py +1 -1
  140. exonware/xwsystem/io/serialization/formats/text/toml.py +1 -1
  141. exonware/xwsystem/io/serialization/formats/text/xml.py +1 -1
  142. exonware/xwsystem/io/serialization/formats/text/yaml.py +1 -1
  143. exonware/xwsystem/io/serialization/registry.py +1 -1
  144. exonware/xwsystem/io/serialization/serializer.py +1 -1
  145. exonware/xwsystem/io/serialization/utils/__init__.py +1 -1
  146. exonware/xwsystem/io/serialization/utils/path_ops.py +1 -1
  147. exonware/xwsystem/io/stream/__init__.py +1 -1
  148. exonware/xwsystem/io/stream/async_operations.py +1 -1
  149. exonware/xwsystem/io/stream/base.py +1 -1
  150. exonware/xwsystem/io/stream/codec_io.py +1 -1
  151. exonware/xwsystem/ipc/async_fabric.py +1 -1
  152. exonware/xwsystem/ipc/base.py +1 -1
  153. exonware/xwsystem/ipc/contracts.py +1 -1
  154. exonware/xwsystem/ipc/defs.py +1 -1
  155. exonware/xwsystem/ipc/errors.py +1 -1
  156. exonware/xwsystem/lazy_bootstrap.py +79 -0
  157. exonware/xwsystem/monitoring/base.py +1 -1
  158. exonware/xwsystem/monitoring/contracts.py +1 -1
  159. exonware/xwsystem/monitoring/defs.py +1 -1
  160. exonware/xwsystem/monitoring/errors.py +1 -1
  161. exonware/xwsystem/monitoring/performance_manager_generic.py +1 -1
  162. exonware/xwsystem/monitoring/system_monitor.py +1 -1
  163. exonware/xwsystem/monitoring/tracing.py +17 -15
  164. exonware/xwsystem/monitoring/tracker.py +1 -1
  165. exonware/xwsystem/operations/__init__.py +1 -1
  166. exonware/xwsystem/operations/base.py +1 -1
  167. exonware/xwsystem/operations/defs.py +1 -1
  168. exonware/xwsystem/operations/diff.py +1 -1
  169. exonware/xwsystem/operations/merge.py +1 -1
  170. exonware/xwsystem/operations/patch.py +1 -1
  171. exonware/xwsystem/patterns/base.py +1 -1
  172. exonware/xwsystem/patterns/contracts.py +1 -1
  173. exonware/xwsystem/patterns/defs.py +1 -1
  174. exonware/xwsystem/patterns/errors.py +1 -1
  175. exonware/xwsystem/patterns/registry.py +1 -1
  176. exonware/xwsystem/plugins/__init__.py +1 -1
  177. exonware/xwsystem/plugins/base.py +1 -1
  178. exonware/xwsystem/plugins/contracts.py +1 -1
  179. exonware/xwsystem/plugins/defs.py +1 -1
  180. exonware/xwsystem/plugins/errors.py +1 -1
  181. exonware/xwsystem/runtime/__init__.py +1 -1
  182. exonware/xwsystem/runtime/base.py +1 -1
  183. exonware/xwsystem/runtime/contracts.py +1 -1
  184. exonware/xwsystem/runtime/defs.py +1 -1
  185. exonware/xwsystem/runtime/env.py +1 -1
  186. exonware/xwsystem/runtime/errors.py +1 -1
  187. exonware/xwsystem/runtime/reflection.py +1 -1
  188. exonware/xwsystem/security/auth.py +1 -1
  189. exonware/xwsystem/security/base.py +1 -1
  190. exonware/xwsystem/security/contracts.py +1 -1
  191. exonware/xwsystem/security/crypto.py +1 -1
  192. exonware/xwsystem/security/defs.py +1 -1
  193. exonware/xwsystem/security/errors.py +1 -1
  194. exonware/xwsystem/security/hazmat.py +1 -1
  195. exonware/xwsystem/shared/__init__.py +1 -1
  196. exonware/xwsystem/shared/base.py +1 -1
  197. exonware/xwsystem/shared/contracts.py +1 -1
  198. exonware/xwsystem/shared/defs.py +1 -1
  199. exonware/xwsystem/shared/errors.py +1 -1
  200. exonware/xwsystem/structures/base.py +1 -1
  201. exonware/xwsystem/structures/contracts.py +1 -1
  202. exonware/xwsystem/structures/defs.py +1 -1
  203. exonware/xwsystem/structures/errors.py +1 -1
  204. exonware/xwsystem/threading/async_primitives.py +1 -1
  205. exonware/xwsystem/threading/base.py +1 -1
  206. exonware/xwsystem/threading/contracts.py +1 -1
  207. exonware/xwsystem/threading/defs.py +1 -1
  208. exonware/xwsystem/threading/errors.py +1 -1
  209. exonware/xwsystem/utils/base.py +1 -1
  210. exonware/xwsystem/utils/contracts.py +1 -1
  211. exonware/xwsystem/utils/dt/__init__.py +1 -1
  212. exonware/xwsystem/utils/dt/base.py +1 -1
  213. exonware/xwsystem/utils/dt/contracts.py +1 -1
  214. exonware/xwsystem/utils/dt/defs.py +1 -1
  215. exonware/xwsystem/utils/dt/errors.py +1 -1
  216. exonware/xwsystem/utils/dt/formatting.py +1 -1
  217. exonware/xwsystem/utils/dt/humanize.py +1 -1
  218. exonware/xwsystem/utils/dt/parsing.py +1 -1
  219. exonware/xwsystem/utils/dt/timezone_utils.py +1 -1
  220. exonware/xwsystem/utils/errors.py +1 -1
  221. exonware/xwsystem/utils/test_runner.py +1 -1
  222. exonware/xwsystem/utils/utils_contracts.py +1 -1
  223. exonware/xwsystem/validation/__init__.py +1 -1
  224. exonware/xwsystem/validation/base.py +1 -1
  225. exonware/xwsystem/validation/contracts.py +1 -1
  226. exonware/xwsystem/validation/declarative.py +1 -1
  227. exonware/xwsystem/validation/defs.py +1 -1
  228. exonware/xwsystem/validation/errors.py +1 -1
  229. exonware/xwsystem/validation/fluent_validator.py +1 -1
  230. exonware/xwsystem/version.py +2 -2
  231. {exonware_xwsystem-0.0.1.407.dist-info → exonware_xwsystem-0.0.1.408.dist-info}/METADATA +3 -3
  232. exonware_xwsystem-0.0.1.408.dist-info/RECORD +274 -0
  233. exonware/xwsystem/_lazy_bootstrap.py +0 -77
  234. exonware/xwsystem/utils/lazy_package/ARCHITECTURE.md +0 -820
  235. exonware/xwsystem/utils/lazy_package/__init__.py +0 -268
  236. exonware/xwsystem/utils/lazy_package/config.py +0 -163
  237. exonware/xwsystem/utils/lazy_package/lazy_base.py +0 -465
  238. exonware/xwsystem/utils/lazy_package/lazy_contracts.py +0 -290
  239. exonware/xwsystem/utils/lazy_package/lazy_core.py +0 -2248
  240. exonware/xwsystem/utils/lazy_package/lazy_errors.py +0 -253
  241. exonware/xwsystem/utils/lazy_package/lazy_state.py +0 -86
  242. exonware_xwsystem-0.0.1.407.dist-info/RECORD +0 -282
  243. {exonware_xwsystem-0.0.1.407.dist-info → exonware_xwsystem-0.0.1.408.dist-info}/WHEEL +0 -0
  244. {exonware_xwsystem-0.0.1.407.dist-info → exonware_xwsystem-0.0.1.408.dist-info}/licenses/LICENSE +0 -0
@@ -1,2248 +0,0 @@
1
- """
2
- #exonware/xwsystem/src/exonware/xwsystem/utils/lazy_package/lazy_core.py
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
- Version: 0.0.1.407
8
- Generation Date: 10-Oct-2025
9
-
10
- Lazy Loading System - Core Implementation
11
-
12
- This module consolidates all lazy loading functionality into a single implementation
13
- following DEV_GUIDELINES.md structure. It provides per-package lazy loading with:
14
- - Automatic dependency discovery
15
- - Secure package installation
16
- - Import hooks for two-stage loading
17
- - Performance monitoring and caching
18
- - SBOM generation and lockfile management
19
-
20
- Design Patterns Applied:
21
- - Facade: LazySystemFacade provides unified API
22
- - Strategy: Pluggable discovery/installation strategies
23
- - Template Method: Base classes define workflows
24
- - Singleton: Global instances for system-wide state
25
- - Registry: Per-package isolation
26
- - Observer: Performance monitoring
27
- - Proxy: Deferred loading
28
-
29
- Core Goal: Per-Package Lazy Loading
30
- - Each package (xwsystem, xwnode, xwdata) can independently enable lazy mode
31
- - Only packages installed with [lazy] extra get auto-installation
32
- - Logs missing imports per package
33
- - Installs on first actual usage (two-stage loading)
34
- """
35
-
36
- import os
37
- import re
38
- import json
39
- import sys
40
- import subprocess
41
- import importlib
42
- import importlib.util
43
- import builtins
44
- import threading
45
- from datetime import datetime
46
- from pathlib import Path
47
- from typing import Dict, List, Optional, Set, Tuple, Any
48
- from types import ModuleType
49
-
50
- from .lazy_contracts import DependencyInfo, LazyInstallMode
51
- from .lazy_errors import (
52
- LazySystemError,
53
- LazyInstallError,
54
- LazyDiscoveryError,
55
- ExternallyManagedError,
56
- DeferredImportError,
57
- )
58
- from .lazy_base import (
59
- APackageDiscovery,
60
- APackageInstaller,
61
- AImportHook,
62
- ALazyLoader,
63
- )
64
- from ...config.logging_setup import get_logger
65
- from .lazy_state import LazyStateManager
66
-
67
- logger = get_logger("xwsystem.utils.lazy_package")
68
-
69
-
70
- # =============================================================================
71
- # SECTION 1: PACKAGE DISCOVERY (~350 lines)
72
- # =============================================================================
73
-
74
- class DependencyMapper:
75
- """
76
- Maps import names to package names using dynamic discovery.
77
- Optimized with caching to avoid repeated file I/O.
78
- """
79
-
80
- __slots__ = ('_discovery', '_package_import_mapping', '_import_package_mapping', '_cached', '_lock')
81
-
82
- def __init__(self):
83
- """Initialize dependency mapper."""
84
- self._discovery = None # Lazy init to avoid circular imports
85
- self._package_import_mapping = {}
86
- self._import_package_mapping = {}
87
- self._cached = False
88
- self._lock = threading.RLock()
89
-
90
- def _get_discovery(self):
91
- """Get discovery instance (lazy init)."""
92
- if self._discovery is None:
93
- self._discovery = get_lazy_discovery()
94
- return self._discovery
95
-
96
- def _ensure_mappings_cached(self) -> None:
97
- """Ensure mappings are cached (lazy initialization)."""
98
- if self._cached:
99
- return
100
-
101
- with self._lock:
102
- if self._cached:
103
- return
104
-
105
- discovery = self._get_discovery()
106
- self._package_import_mapping = discovery.get_package_import_mapping()
107
- self._import_package_mapping = discovery.get_import_package_mapping()
108
- self._cached = True
109
-
110
- def get_package_name(self, import_name: str) -> Optional[str]:
111
- """Get package name from import name."""
112
- if import_name in LazyDiscovery.SYSTEM_MODULES_BLACKLIST:
113
- logger.debug(f"Module '{import_name}' is in system blacklist, skipping auto-install")
114
- return None
115
-
116
- self._ensure_mappings_cached()
117
- return self._import_package_mapping.get(import_name, import_name)
118
-
119
- def get_import_names(self, package_name: str) -> List[str]:
120
- """Get all possible import names for a package."""
121
- self._ensure_mappings_cached()
122
- return self._package_import_mapping.get(package_name, [package_name])
123
-
124
- def get_package_import_mapping(self) -> Dict[str, List[str]]:
125
- """Get complete package to import names mapping."""
126
- self._ensure_mappings_cached()
127
- return self._package_import_mapping.copy()
128
-
129
- def get_import_package_mapping(self) -> Dict[str, str]:
130
- """Get complete import to package name mapping."""
131
- self._ensure_mappings_cached()
132
- return self._import_package_mapping.copy()
133
-
134
-
135
- class LazyDiscovery(APackageDiscovery):
136
- """
137
- Discovers dependencies from project configuration sources.
138
- Implements caching with file modification time checks.
139
- """
140
-
141
- # System/built-in modules that should NEVER be auto-installed
142
- SYSTEM_MODULES_BLACKLIST = {
143
- 'pwd', 'grp', 'spwd', 'crypt', 'nis', 'syslog', 'termios', 'tty', 'pty',
144
- 'fcntl', 'resource', 'msvcrt', 'winreg', 'winsound', '_winapi',
145
- 'rpython', 'rply', 'rnc2rng', '_dbm',
146
- 'sys', 'os', 'io', 'time', 'datetime', 'json', 'csv', 'math',
147
- 'random', 're', 'collections', 'itertools', 'functools', 'operator',
148
- 'pathlib', 'shutil', 'glob', 'tempfile', 'pickle', 'copy', 'types',
149
- 'typing', 'abc', 'enum', 'dataclasses', 'contextlib', 'warnings',
150
- 'logging', 'threading', 'multiprocessing', 'subprocess', 'queue',
151
- 'socket', 'select', 'signal', 'asyncio', 'concurrent', 'email',
152
- 'http', 'urllib', 'xml', 'html', 'sqlite3', 'base64', 'hashlib',
153
- 'hmac', 'secrets', 'ssl', 'binascii', 'struct', 'array', 'weakref',
154
- 'gc', 'inspect', 'traceback', 'atexit', 'codecs', 'locale', 'gettext',
155
- 'argparse', 'optparse', 'configparser', 'fileinput', 'stat', 'platform',
156
- 'unittest', 'doctest', 'pdb', 'profile', 'cProfile', 'timeit', 'trace',
157
- # Internal / optional modules that must never trigger auto-install
158
- 'compression', 'socks', 'wimlib',
159
- }
160
-
161
- # Common import name to package name mappings
162
- COMMON_MAPPINGS = {
163
- 'cv2': 'opencv-python',
164
- 'PIL': 'Pillow',
165
- 'Pillow': 'Pillow',
166
- 'yaml': 'PyYAML',
167
- 'sklearn': 'scikit-learn',
168
- 'bs4': 'beautifulsoup4',
169
- 'dateutil': 'python-dateutil',
170
- 'requests_oauthlib': 'requests-oauthlib',
171
- 'google': 'google-api-python-client',
172
- 'jwt': 'PyJWT',
173
- 'crypto': 'pycrypto',
174
- 'Crypto': 'pycrypto',
175
- 'MySQLdb': 'mysqlclient',
176
- 'psycopg2': 'psycopg2-binary',
177
- 'bson': 'pymongo',
178
- 'lxml': 'lxml',
179
- 'numpy': 'numpy',
180
- 'pandas': 'pandas',
181
- 'matplotlib': 'matplotlib',
182
- 'seaborn': 'seaborn',
183
- 'plotly': 'plotly',
184
- 'django': 'Django',
185
- 'flask': 'Flask',
186
- 'fastapi': 'fastapi',
187
- 'uvicorn': 'uvicorn',
188
- 'pytest': 'pytest',
189
- 'black': 'black',
190
- 'isort': 'isort',
191
- 'mypy': 'mypy',
192
- 'psutil': 'psutil',
193
- 'colorama': 'colorama',
194
- 'pytz': 'pytz',
195
- 'aiofiles': 'aiofiles',
196
- 'watchdog': 'watchdog',
197
- 'wand': 'Wand',
198
- 'exifread': 'ExifRead',
199
- 'piexif': 'piexif',
200
- 'rawpy': 'rawpy',
201
- 'imageio': 'imageio',
202
- 'scipy': 'scipy',
203
- 'scikit-image': 'scikit-image',
204
- 'opencv-python': 'opencv-python',
205
- 'opencv-contrib-python': 'opencv-contrib-python',
206
- }
207
-
208
- def _discover_from_sources(self) -> None:
209
- """Discover dependencies from all sources."""
210
- self._discover_from_pyproject_toml()
211
- self._discover_from_requirements_txt()
212
- self._discover_from_setup_py()
213
- self._discover_from_custom_config()
214
-
215
- def _is_cache_valid(self) -> bool:
216
- """Check if cached dependencies are still valid."""
217
- if not self._cache_valid or not self._cached_dependencies:
218
- return False
219
-
220
- config_files = [
221
- self.project_root / 'pyproject.toml',
222
- self.project_root / 'requirements.txt',
223
- self.project_root / 'setup.py',
224
- ]
225
-
226
- for config_file in config_files:
227
- if config_file.exists():
228
- try:
229
- current_mtime = config_file.stat().st_mtime
230
- cached_mtime = self._file_mtimes.get(str(config_file), 0)
231
- if current_mtime > cached_mtime:
232
- return False
233
- except:
234
- return False
235
-
236
- return True
237
-
238
- def _update_file_mtimes(self) -> None:
239
- """Update file modification times for cache validation."""
240
- config_files = [
241
- self.project_root / 'pyproject.toml',
242
- self.project_root / 'requirements.txt',
243
- self.project_root / 'setup.py',
244
- ]
245
- for config_file in config_files:
246
- if config_file.exists():
247
- try:
248
- self._file_mtimes[str(config_file)] = config_file.stat().st_mtime
249
- except:
250
- pass
251
-
252
- def _discover_from_pyproject_toml(self) -> None:
253
- """Discover dependencies from pyproject.toml."""
254
- pyproject_path = self.project_root / 'pyproject.toml'
255
- if not pyproject_path.exists():
256
- return
257
-
258
- try:
259
- try:
260
- import tomllib # Python 3.11+
261
- toml_parser = tomllib # type: ignore[assignment]
262
- except ImportError:
263
- try:
264
- import tomli as tomllib # type: ignore[assignment]
265
- toml_parser = tomllib
266
- except ImportError:
267
- logger.info(
268
- "TOML parser not available; attempting to lazy-install 'tomli'..."
269
- )
270
- try:
271
- subprocess.run(
272
- [sys.executable, "-m", "pip", "install", "tomli"],
273
- check=False,
274
- capture_output=True,
275
- )
276
- import tomli as tomllib # type: ignore[assignment]
277
- toml_parser = tomllib
278
- except Exception as install_exc:
279
- logger.warning(
280
- "tomli installation failed; skipping pyproject.toml discovery "
281
- f"({install_exc})"
282
- )
283
- return
284
-
285
- with open(pyproject_path, 'rb') as f:
286
- data = toml_parser.load(f)
287
-
288
- dependencies = []
289
- if 'project' in data and 'dependencies' in data['project']:
290
- dependencies.extend(data['project']['dependencies'])
291
-
292
- if 'project' in data and 'optional-dependencies' in data['project']:
293
- for group_name, group_deps in data['project']['optional-dependencies'].items():
294
- dependencies.extend(group_deps)
295
-
296
- if 'build-system' in data and 'requires' in data['build-system']:
297
- dependencies.extend(data['build-system']['requires'])
298
-
299
- for dep in dependencies:
300
- self._parse_dependency_string(dep, 'pyproject.toml')
301
-
302
- self._discovery_sources.append('pyproject.toml')
303
- except Exception as e:
304
- logger.warning(f"Could not parse pyproject.toml: {e}")
305
-
306
- def _discover_from_requirements_txt(self) -> None:
307
- """Discover dependencies from requirements.txt."""
308
- requirements_path = self.project_root / 'requirements.txt'
309
- if not requirements_path.exists():
310
- return
311
-
312
- try:
313
- with open(requirements_path, 'r', encoding='utf-8') as f:
314
- for line in f:
315
- line = line.strip()
316
- if line and not line.startswith('#'):
317
- self._parse_dependency_string(line, 'requirements.txt')
318
-
319
- self._discovery_sources.append('requirements.txt')
320
- except Exception as e:
321
- logger.warning(f"Could not parse requirements.txt: {e}")
322
-
323
- def _discover_from_setup_py(self) -> None:
324
- """Discover dependencies from setup.py."""
325
- setup_path = self.project_root / 'setup.py'
326
- if not setup_path.exists():
327
- return
328
-
329
- try:
330
- with open(setup_path, 'r', encoding='utf-8') as f:
331
- content = f.read()
332
-
333
- install_requires_match = re.search(
334
- r'install_requires\s*=\s*\[(.*?)\]',
335
- content,
336
- re.DOTALL
337
- )
338
- if install_requires_match:
339
- deps_str = install_requires_match.group(1)
340
- deps = re.findall(r'["\']([^"\']+)["\']', deps_str)
341
- for dep in deps:
342
- self._parse_dependency_string(dep, 'setup.py')
343
-
344
- self._discovery_sources.append('setup.py')
345
- except Exception as e:
346
- logger.warning(f"Could not parse setup.py: {e}")
347
-
348
- def _discover_from_custom_config(self) -> None:
349
- """Discover dependencies from custom configuration files."""
350
- config_files = [
351
- 'dependency-mappings.json',
352
- 'lazy-dependencies.json',
353
- 'dependencies.json'
354
- ]
355
-
356
- for config_file in config_files:
357
- config_path = self.project_root / config_file
358
- if config_path.exists():
359
- try:
360
- with open(config_path, 'r', encoding='utf-8') as f:
361
- data = json.load(f)
362
-
363
- if isinstance(data, dict):
364
- for import_name, package_name in data.items():
365
- self.discovered_dependencies[import_name] = DependencyInfo(
366
- import_name=import_name,
367
- package_name=package_name,
368
- source=config_file,
369
- category='custom'
370
- )
371
-
372
- self._discovery_sources.append(config_file)
373
- except Exception as e:
374
- logger.warning(f"Could not parse {config_file}: {e}")
375
-
376
- def _parse_dependency_string(self, dep_str: str, source: str) -> None:
377
- """Parse a dependency string and extract dependency information."""
378
- dep_str = re.sub(r'[>=<!=~]+.*', '', dep_str)
379
- dep_str = re.sub(r'\[.*\]', '', dep_str)
380
- dep_str = dep_str.strip()
381
-
382
- if not dep_str:
383
- return
384
-
385
- import_name = dep_str
386
- package_name = dep_str
387
-
388
- if dep_str in self.COMMON_MAPPINGS:
389
- package_name = self.COMMON_MAPPINGS[dep_str]
390
- elif dep_str in self.COMMON_MAPPINGS.values():
391
- for imp_name, pkg_name in self.COMMON_MAPPINGS.items():
392
- if pkg_name == dep_str:
393
- import_name = imp_name
394
- break
395
-
396
- self.discovered_dependencies[import_name] = DependencyInfo(
397
- import_name=import_name,
398
- package_name=package_name,
399
- source=source,
400
- category='discovered'
401
- )
402
-
403
- def _add_common_mappings(self) -> None:
404
- """Add common mappings that might not be in dependency files."""
405
- for import_name, package_name in self.COMMON_MAPPINGS.items():
406
- if import_name not in self.discovered_dependencies:
407
- self.discovered_dependencies[import_name] = DependencyInfo(
408
- import_name=import_name,
409
- package_name=package_name,
410
- source='common_mappings',
411
- category='common'
412
- )
413
-
414
- def get_package_for_import(self, import_name: str) -> Optional[str]:
415
- """Get package name for a given import name."""
416
- mapping = self.discover_all_dependencies()
417
- return mapping.get(import_name)
418
-
419
- def get_imports_for_package(self, package_name: str) -> List[str]:
420
- """Get all possible import names for a package."""
421
- mapping = self.get_package_import_mapping()
422
- return mapping.get(package_name, [package_name])
423
-
424
- def get_package_import_mapping(self) -> Dict[str, List[str]]:
425
- """Get mapping of package names to their possible import names."""
426
- self.discover_all_dependencies()
427
-
428
- package_to_imports = {}
429
- for import_name, dep_info in self.discovered_dependencies.items():
430
- package_name = dep_info.package_name
431
-
432
- if package_name not in package_to_imports:
433
- package_to_imports[package_name] = [package_name]
434
-
435
- if import_name != package_name:
436
- if import_name not in package_to_imports[package_name]:
437
- package_to_imports[package_name].append(import_name)
438
-
439
- return package_to_imports
440
-
441
- def get_import_package_mapping(self) -> Dict[str, str]:
442
- """Get mapping of import names to package names."""
443
- self.discover_all_dependencies()
444
- return {import_name: dep_info.package_name for import_name, dep_info in self.discovered_dependencies.items()}
445
-
446
- def export_to_json(self, file_path: str) -> None:
447
- """Export discovered dependencies to JSON file."""
448
- data = {
449
- 'dependencies': {name: info.package_name for name, info in self.discovered_dependencies.items()},
450
- 'sources': self.get_discovery_sources(),
451
- 'total_count': len(self.discovered_dependencies)
452
- }
453
-
454
- with open(file_path, 'w', encoding='utf-8') as f:
455
- json.dump(data, f, indent=2, ensure_ascii=False)
456
-
457
-
458
- # Global discovery instance
459
- _discovery = None
460
- _discovery_lock = threading.RLock()
461
-
462
- def get_lazy_discovery(project_root: Optional[str] = None) -> LazyDiscovery:
463
- """Get the global lazy discovery instance."""
464
- global _discovery
465
- if _discovery is None:
466
- with _discovery_lock:
467
- if _discovery is None:
468
- _discovery = LazyDiscovery(project_root)
469
- return _discovery
470
-
471
-
472
- def discover_dependencies(project_root: Optional[str] = None) -> Dict[str, str]:
473
- """Discover all dependencies for the current project."""
474
- discovery = get_lazy_discovery(project_root)
475
- return discovery.discover_all_dependencies()
476
-
477
-
478
- def export_dependency_mappings(file_path: str, project_root: Optional[str] = None) -> None:
479
- """Export discovered dependency mappings to a JSON file."""
480
- discovery = get_lazy_discovery(project_root)
481
- discovery.export_to_json(file_path)
482
-
483
-
484
- # =============================================================================
485
- # SECTION 2: PACKAGE INSTALLATION (~550 lines)
486
- # =============================================================================
487
-
488
- def _is_externally_managed() -> bool:
489
- """Check if Python environment is externally managed (PEP 668)."""
490
- marker_file = Path(sys.prefix) / "EXTERNALLY-MANAGED"
491
- return marker_file.exists()
492
-
493
-
494
- def _check_pip_audit_available() -> bool:
495
- """Check if pip-audit is available for vulnerability scanning."""
496
- try:
497
- result = subprocess.run(
498
- [sys.executable, '-m', 'pip', 'list'],
499
- capture_output=True,
500
- text=True,
501
- timeout=5
502
- )
503
- return 'pip-audit' in result.stdout
504
- except Exception:
505
- return False
506
-
507
-
508
- class LazyInstallPolicy:
509
- """
510
- Security and policy configuration for lazy installation.
511
- Per-package allow/deny lists, index URLs, and security settings.
512
- """
513
- __slots__ = ()
514
-
515
- _allow_lists: Dict[str, Set[str]] = {}
516
- _deny_lists: Dict[str, Set[str]] = {}
517
- _index_urls: Dict[str, str] = {}
518
- _extra_index_urls: Dict[str, List[str]] = {}
519
- _trusted_hosts: Dict[str, List[str]] = {}
520
- _require_hashes: Dict[str, bool] = {}
521
- _verify_ssl: Dict[str, bool] = {}
522
- _lockfile_paths: Dict[str, str] = {}
523
- _lock = threading.RLock()
524
-
525
- @classmethod
526
- def set_allow_list(cls, package_name: str, allowed_packages: List[str]) -> None:
527
- """Set allow list for a package (only these can be installed)."""
528
- with cls._lock:
529
- cls._allow_lists[package_name] = set(allowed_packages)
530
- logger.info(f"Set allow list for {package_name}: {len(allowed_packages)} packages")
531
-
532
- @classmethod
533
- def set_deny_list(cls, package_name: str, denied_packages: List[str]) -> None:
534
- """Set deny list for a package (these cannot be installed)."""
535
- with cls._lock:
536
- cls._deny_lists[package_name] = set(denied_packages)
537
- logger.info(f"Set deny list for {package_name}: {len(denied_packages)} packages")
538
-
539
- @classmethod
540
- def add_to_allow_list(cls, package_name: str, allowed_package: str) -> None:
541
- """Add single package to allow list."""
542
- with cls._lock:
543
- if package_name not in cls._allow_lists:
544
- cls._allow_lists[package_name] = set()
545
- cls._allow_lists[package_name].add(allowed_package)
546
-
547
- @classmethod
548
- def add_to_deny_list(cls, package_name: str, denied_package: str) -> None:
549
- """Add single package to deny list."""
550
- with cls._lock:
551
- if package_name not in cls._deny_lists:
552
- cls._deny_lists[package_name] = set()
553
- cls._deny_lists[package_name].add(denied_package)
554
-
555
- @classmethod
556
- def is_package_allowed(cls, installer_package: str, target_package: str) -> Tuple[bool, str]:
557
- """Check if target_package can be installed by installer_package."""
558
- with cls._lock:
559
- if installer_package in cls._deny_lists:
560
- if target_package in cls._deny_lists[installer_package]:
561
- return False, f"Package '{target_package}' is in deny list"
562
-
563
- if installer_package in cls._allow_lists:
564
- if target_package not in cls._allow_lists[installer_package]:
565
- return False, f"Package '{target_package}' not in allow list"
566
-
567
- return True, "OK"
568
-
569
- @classmethod
570
- def set_index_url(cls, package_name: str, index_url: str) -> None:
571
- """Set PyPI index URL for a package."""
572
- with cls._lock:
573
- cls._index_urls[package_name] = index_url
574
- logger.info(f"Set index URL for {package_name}: {index_url}")
575
-
576
- @classmethod
577
- def set_extra_index_urls(cls, package_name: str, urls: List[str]) -> None:
578
- """Set extra index URLs for a package."""
579
- with cls._lock:
580
- cls._extra_index_urls[package_name] = urls
581
- logger.info(f"Set {len(urls)} extra index URLs for {package_name}")
582
-
583
- @classmethod
584
- def add_trusted_host(cls, package_name: str, host: str) -> None:
585
- """Add trusted host for a package."""
586
- with cls._lock:
587
- if package_name not in cls._trusted_hosts:
588
- cls._trusted_hosts[package_name] = []
589
- cls._trusted_hosts[package_name].append(host)
590
-
591
- @classmethod
592
- def get_pip_args(cls, package_name: str) -> List[str]:
593
- """Get pip install arguments for a package based on policy."""
594
- args = []
595
-
596
- with cls._lock:
597
- if package_name in cls._index_urls:
598
- args.extend(['--index-url', cls._index_urls[package_name]])
599
-
600
- if package_name in cls._extra_index_urls:
601
- for url in cls._extra_index_urls[package_name]:
602
- args.extend(['--extra-index-url', url])
603
-
604
- if package_name in cls._trusted_hosts:
605
- for host in cls._trusted_hosts[package_name]:
606
- args.extend(['--trusted-host', host])
607
-
608
- if cls._require_hashes.get(package_name, False):
609
- args.append('--require-hashes')
610
-
611
- if not cls._verify_ssl.get(package_name, True):
612
- args.append('--no-verify-ssl')
613
-
614
- return args
615
-
616
- @classmethod
617
- def set_lockfile_path(cls, package_name: str, path: str) -> None:
618
- """Set lockfile path for a package."""
619
- with cls._lock:
620
- cls._lockfile_paths[package_name] = path
621
-
622
- @classmethod
623
- def get_lockfile_path(cls, package_name: str) -> Optional[str]:
624
- """Get lockfile path for a package."""
625
- with cls._lock:
626
- return cls._lockfile_paths.get(package_name)
627
-
628
-
629
- class LazyInstaller(APackageInstaller):
630
- """
631
- Lazy installer that automatically installs missing packages on import failure.
632
- Each instance is isolated per package to prevent interference.
633
- """
634
-
635
- __slots__ = APackageInstaller.__slots__ + ('_dependency_mapper', '_auto_approve_all')
636
-
637
- def __init__(self, package_name: str = 'default'):
638
- """Initialize lazy installer for a specific package."""
639
- super().__init__(package_name)
640
- self._dependency_mapper = DependencyMapper()
641
- self._auto_approve_all = False
642
-
643
- def _ask_user_permission(self, package_name: str, module_name: str) -> bool:
644
- """Ask user for permission to install a package."""
645
- if self._auto_approve_all:
646
- return True
647
-
648
- print(f"\n{'='*60}")
649
- print(f"Lazy Installation Active - {self._package_name}")
650
- print(f"{'='*60}")
651
- print(f"Package: {package_name}")
652
- print(f"Module: {module_name}")
653
- print(f"{'='*60}")
654
- print(f"\nThe module '{module_name}' is not installed.")
655
- print(f"Would you like to install '{package_name}'?")
656
- print(f"\nOptions:")
657
- print(f" [Y] Yes - Install this package")
658
- print(f" [N] No - Skip this package")
659
- print(f" [A] All - Install this and all future packages without asking")
660
- print(f" [Q] Quit - Cancel and raise ImportError")
661
- print(f"{'='*60}")
662
-
663
- while True:
664
- try:
665
- choice = input("Your choice [Y/N/A/Q]: ").strip().upper()
666
-
667
- if choice in ('Y', 'YES', ''):
668
- return True
669
- elif choice in ('N', 'NO'):
670
- return False
671
- elif choice in ('A', 'ALL'):
672
- self._auto_approve_all = True
673
- return True
674
- elif choice in ('Q', 'QUIT'):
675
- raise KeyboardInterrupt("User cancelled installation")
676
- else:
677
- print(f"Invalid choice '{choice}'. Please enter Y, N, A, or Q.")
678
- except (EOFError, KeyboardInterrupt):
679
- print("\n❌ Installation cancelled by user")
680
- return False
681
-
682
- def install_package(self, package_name: str, module_name: str = None) -> bool:
683
- """Install a package using pip."""
684
- with self._lock:
685
- if package_name in self._installed_packages:
686
- return True
687
-
688
- if package_name in self._failed_packages:
689
- return False
690
-
691
- if self._mode == LazyInstallMode.DISABLED:
692
- logger.info(f"Lazy installation disabled for {self._package_name}, skipping {package_name}")
693
- return False
694
-
695
- if self._mode == LazyInstallMode.WARN:
696
- logger.warning(f"[WARN] Package '{package_name}' is missing but WARN mode is active - not installing")
697
- print(f"[WARN] ({self._package_name}): Package '{package_name}' is missing (not installed in WARN mode)")
698
- return False
699
-
700
- if self._mode == LazyInstallMode.DRY_RUN:
701
- print(f"[DRY RUN] ({self._package_name}): Would install package '{package_name}'")
702
- return False
703
-
704
- if self._mode == LazyInstallMode.INTERACTIVE:
705
- if not self._ask_user_permission(package_name, module_name or package_name):
706
- logger.info(f"User declined installation of {package_name}")
707
- self._failed_packages.add(package_name)
708
- return False
709
-
710
- # Security checks
711
- if _is_externally_managed():
712
- logger.error(f"Cannot install {package_name}: Environment is externally managed (PEP 668)")
713
- print(f"\n[ERROR] This Python environment is externally managed (PEP 668)")
714
- print(f"Package '{package_name}' cannot be installed in this environment.")
715
- print(f"\nSuggested solutions:")
716
- print(f" 1. Create a virtual environment:")
717
- print(f" python -m venv .venv")
718
- print(f" .venv\\Scripts\\activate # Windows")
719
- print(f" source .venv/bin/activate # Linux/macOS")
720
- print(f" 2. Use pipx for isolated installs:")
721
- print(f" pipx install {package_name}")
722
- print(f" 3. Override with --break-system-packages (NOT RECOMMENDED)\n")
723
- self._failed_packages.add(package_name)
724
- return False
725
-
726
- allowed, reason = LazyInstallPolicy.is_package_allowed(self._package_name, package_name)
727
- if not allowed:
728
- logger.error(f"Cannot install {package_name}: {reason}")
729
- print(f"\n[SECURITY] Package '{package_name}' blocked: {reason}\n")
730
- self._failed_packages.add(package_name)
731
- return False
732
-
733
- # Proceed with installation
734
- try:
735
- logger.info(f"Installing package: {package_name}")
736
- print(f"\n[INSTALL] Installing {package_name}...")
737
-
738
- pip_args = [sys.executable, '-m', 'pip', 'install']
739
- policy_args = LazyInstallPolicy.get_pip_args(self._package_name)
740
- if policy_args:
741
- pip_args.extend(policy_args)
742
- logger.debug(f"Using policy args: {policy_args}")
743
-
744
- pip_args.append(package_name)
745
-
746
- result = subprocess.run(
747
- pip_args,
748
- capture_output=True,
749
- text=True,
750
- check=True
751
- )
752
-
753
- self._installed_packages.add(package_name)
754
- print(f"[OK] Successfully installed: {package_name}\n")
755
- logger.info(f"Successfully installed: {package_name}")
756
-
757
- if _check_pip_audit_available():
758
- self._run_vulnerability_audit(package_name)
759
-
760
- self._update_lockfile(package_name)
761
-
762
- return True
763
-
764
- except subprocess.CalledProcessError as e:
765
- logger.error(f"Failed to install {package_name}: {e.stderr}")
766
- print(f"[FAIL] Failed to install {package_name}\n")
767
- self._failed_packages.add(package_name)
768
- return False
769
- except Exception as e:
770
- logger.error(f"Unexpected error installing {package_name}: {e}")
771
- print(f"[ERROR] Unexpected error: {e}\n")
772
- self._failed_packages.add(package_name)
773
- return False
774
-
775
- def _run_vulnerability_audit(self, package_name: str) -> None:
776
- """Run vulnerability audit on installed package using pip-audit."""
777
- try:
778
- result = subprocess.run(
779
- [sys.executable, '-m', 'pip_audit', '-r', '-', '--format', 'json'],
780
- input=package_name,
781
- capture_output=True,
782
- text=True,
783
- timeout=30
784
- )
785
-
786
- if result.returncode == 0:
787
- logger.info(f"Vulnerability audit passed for {package_name}")
788
- else:
789
- try:
790
- audit_data = json.loads(result.stdout)
791
- if audit_data.get('vulnerabilities'):
792
- logger.warning(f"[SECURITY] Vulnerabilities found in {package_name}: {audit_data}")
793
- print(f"[SECURITY WARNING] Package '{package_name}' has known vulnerabilities")
794
- print(f"Run 'pip-audit' for details")
795
- except json.JSONDecodeError:
796
- logger.warning(f"Could not parse audit results for {package_name}")
797
- except subprocess.TimeoutExpired:
798
- logger.warning(f"Vulnerability audit timed out for {package_name}")
799
- except Exception as e:
800
- logger.debug(f"Vulnerability audit skipped for {package_name}: {e}")
801
-
802
- def _update_lockfile(self, package_name: str) -> None:
803
- """Update lockfile with newly installed package."""
804
- lockfile_path = LazyInstallPolicy.get_lockfile_path(self._package_name)
805
- if not lockfile_path:
806
- return
807
-
808
- try:
809
- version = self._get_installed_version(package_name)
810
- if not version:
811
- return
812
-
813
- lockfile_path = Path(lockfile_path)
814
- if lockfile_path.exists():
815
- with open(lockfile_path, 'r', encoding='utf-8') as f:
816
- lockdata = json.load(f)
817
- else:
818
- lockdata = {
819
- "metadata": {
820
- "generated_by": f"xwsystem-lazy-{self._package_name}",
821
- "version": "1.0"
822
- },
823
- "packages": {}
824
- }
825
-
826
- lockdata["packages"][package_name] = {
827
- "version": version,
828
- "installed_at": datetime.now().isoformat(),
829
- "installer": self._package_name
830
- }
831
-
832
- lockfile_path.parent.mkdir(parents=True, exist_ok=True)
833
- with open(lockfile_path, 'w', encoding='utf-8') as f:
834
- json.dump(lockdata, f, indent=2)
835
-
836
- logger.info(f"Updated lockfile: {lockfile_path}")
837
- except Exception as e:
838
- logger.warning(f"Failed to update lockfile: {e}")
839
-
840
- def _get_installed_version(self, package_name: str) -> Optional[str]:
841
- """Get installed version of a package."""
842
- try:
843
- result = subprocess.run(
844
- [sys.executable, '-m', 'pip', 'show', package_name],
845
- capture_output=True,
846
- text=True,
847
- timeout=5
848
- )
849
-
850
- if result.returncode == 0:
851
- for line in result.stdout.split('\n'):
852
- if line.startswith('Version:'):
853
- return line.split(':', 1)[1].strip()
854
- except Exception as e:
855
- logger.debug(f"Could not get version for {package_name}: {e}")
856
- return None
857
-
858
- def generate_sbom(self) -> Dict:
859
- """Generate Software Bill of Materials (SBOM) for installed packages."""
860
- sbom = {
861
- "metadata": {
862
- "format": "xwsystem-sbom",
863
- "version": "1.0",
864
- "generated_at": datetime.now().isoformat(),
865
- "installer_package": self._package_name
866
- },
867
- "packages": []
868
- }
869
-
870
- for pkg in self._installed_packages:
871
- version = self._get_installed_version(pkg)
872
- sbom["packages"].append({
873
- "name": pkg,
874
- "version": version or "unknown",
875
- "installed_by": self._package_name,
876
- "source": "pypi"
877
- })
878
-
879
- return sbom
880
-
881
- def export_sbom(self, output_path: str) -> bool:
882
- """Export SBOM to file."""
883
- try:
884
- sbom = self.generate_sbom()
885
- output_path = Path(output_path)
886
- output_path.parent.mkdir(parents=True, exist_ok=True)
887
-
888
- with open(output_path, 'w', encoding='utf-8') as f:
889
- json.dump(sbom, f, indent=2)
890
-
891
- logger.info(f"Exported SBOM to: {output_path}")
892
- return True
893
- except Exception as e:
894
- logger.error(f"Failed to export SBOM: {e}")
895
- return False
896
-
897
- def is_package_installed(self, package_name: str) -> bool:
898
- """Check if a package is already installed."""
899
- return package_name in self._installed_packages
900
-
901
- def install_and_import(self, module_name: str, package_name: str = None) -> Tuple[Optional[ModuleType], bool]:
902
- """Install package and import module."""
903
- if not self.is_enabled():
904
- return None, False
905
-
906
- if package_name is None:
907
- package_name = self._dependency_mapper.get_package_name(module_name)
908
- if package_name is None:
909
- logger.debug(f"Module '{module_name}' is a system/built-in module, not installing")
910
- return None, False
911
-
912
- try:
913
- module = importlib.import_module(module_name)
914
- return module, True
915
- except ImportError:
916
- pass
917
-
918
- if self.install_package(package_name, module_name):
919
- try:
920
- module = importlib.import_module(module_name)
921
- return module, True
922
- except ImportError as e:
923
- logger.error(f"Still cannot import {module_name} after installing {package_name}: {e}")
924
- return None, False
925
-
926
- return None, False
927
-
928
- def _check_security_policy(self, package_name: str) -> Tuple[bool, str]:
929
- """Check security policy for package."""
930
- return LazyInstallPolicy.is_package_allowed(self._package_name, package_name)
931
-
932
- def _run_pip_install(self, package_name: str, args: List[str]) -> bool:
933
- """Run pip install with arguments."""
934
- try:
935
- pip_args = [sys.executable, '-m', 'pip', 'install'] + args + [package_name]
936
- result = subprocess.run(
937
- pip_args,
938
- capture_output=True,
939
- text=True,
940
- check=True
941
- )
942
- return result.returncode == 0
943
- except subprocess.CalledProcessError:
944
- return False
945
-
946
-
947
- class LazyInstallerRegistry:
948
- """Registry to manage separate lazy installer instances per package."""
949
- _instances: Dict[str, LazyInstaller] = {}
950
- _lock = threading.RLock()
951
-
952
- @classmethod
953
- def get_instance(cls, package_name: str = 'default') -> LazyInstaller:
954
- """Get or create a lazy installer instance for a package."""
955
- with cls._lock:
956
- if package_name not in cls._instances:
957
- cls._instances[package_name] = LazyInstaller(package_name)
958
- return cls._instances[package_name]
959
-
960
- @classmethod
961
- def get_all_instances(cls) -> Dict[str, LazyInstaller]:
962
- """Get all lazy installer instances."""
963
- with cls._lock:
964
- return cls._instances.copy()
965
-
966
-
967
- # =============================================================================
968
- # SECTION 3: IMPORT HOOKS & TWO-STAGE LOADING (~450 lines)
969
- # =============================================================================
970
-
971
- # Global import tracking cache - Prevents infinite loops
972
- _import_in_progress: Dict[Tuple[int, str], bool] = {}
973
- _import_cache_lock = threading.RLock()
974
- _importing = threading.local()
975
-
976
- def _is_import_in_progress(module_name: str) -> bool:
977
- """Check if a module import is currently in progress for this thread."""
978
- thread_id = threading.get_ident()
979
- key = (thread_id, module_name)
980
- with _import_cache_lock:
981
- return key in _import_in_progress
982
-
983
- def _mark_import_started(module_name: str) -> None:
984
- """Mark a module import as started for this thread."""
985
- thread_id = threading.get_ident()
986
- key = (thread_id, module_name)
987
- with _import_cache_lock:
988
- _import_in_progress[key] = True
989
-
990
- def _mark_import_finished(module_name: str) -> None:
991
- """Mark a module import as finished for this thread."""
992
- thread_id = threading.get_ident()
993
- key = (thread_id, module_name)
994
- with _import_cache_lock:
995
- _import_in_progress.pop(key, None)
996
-
997
-
998
- class LazyImportHook(AImportHook):
999
- """
1000
- Import hook that intercepts ImportError and auto-installs packages.
1001
- Performance optimized with zero overhead for successful imports.
1002
- """
1003
-
1004
- __slots__ = AImportHook.__slots__
1005
-
1006
- def handle_import_error(self, module_name: str) -> Optional[Any]:
1007
- """Handle ImportError by attempting to install and re-import."""
1008
- if not self._enabled:
1009
- return None
1010
-
1011
- try:
1012
- module, success = lazy_import_with_install(
1013
- module_name,
1014
- installer_package=self._package_name
1015
- )
1016
- return module if success else None
1017
- except:
1018
- return None
1019
-
1020
- def install_hook(self) -> None:
1021
- """Install the import hook into sys.meta_path."""
1022
- install_import_hook(self._package_name)
1023
-
1024
- def uninstall_hook(self) -> None:
1025
- """Uninstall the import hook from sys.meta_path."""
1026
- uninstall_import_hook(self._package_name)
1027
-
1028
- def is_installed(self) -> bool:
1029
- """Check if hook is installed."""
1030
- return is_import_hook_installed(self._package_name)
1031
-
1032
-
1033
- class LazyMetaPathFinder:
1034
- """
1035
- Custom meta path finder that intercepts failed imports.
1036
- Performance optimized - only triggers when import would fail anyway.
1037
- """
1038
-
1039
- __slots__ = ('_package_name', '_enabled')
1040
-
1041
- def __init__(self, package_name: str = 'default'):
1042
- """Initialize meta path finder."""
1043
- self._package_name = package_name
1044
- self._enabled = True
1045
-
1046
- def find_module(self, fullname: str, path: Optional[str] = None):
1047
- """Find module - returns None to let standard import continue."""
1048
- return None
1049
-
1050
- def find_spec(self, fullname: str, path: Optional[str] = None, target=None):
1051
- """Find module spec - intercepts imports to enable two-stage lazy loading."""
1052
- if not self._enabled:
1053
- return None
1054
-
1055
- if _is_import_in_progress(fullname):
1056
- logger.debug(f"[RECURSION GUARD] Import '{fullname}' already in progress, skipping hook")
1057
- return None
1058
-
1059
- # Two-stage lazy loading for serialization and archive modules
1060
- # Root cause fix: Hook was watching wrong path (exonware.xwsystem.serialization.*)
1061
- # Actual path is exonware.xwsystem.io.serialization.*
1062
- # Also watch archive formats to handle optional dependencies like wimlib
1063
- # Priority impact: Usability (#2) - Missing dependencies not auto-installed
1064
- watched_prefixes = [
1065
- 'exonware.xwsystem.io.serialization.',
1066
- 'exonware.xwsystem.io.archive.formats.',
1067
- ]
1068
-
1069
- for prefix in watched_prefixes:
1070
- if fullname.startswith(prefix):
1071
- module_suffix = fullname[len(prefix):]
1072
-
1073
- if module_suffix and '.' not in module_suffix:
1074
- logger.info(f"[HOOK] Candidate for wrapping: {fullname}")
1075
-
1076
- _mark_import_started(fullname)
1077
- try:
1078
- if getattr(_importing, 'active', False):
1079
- logger.debug(f"[HOOK] Recursion guard active, skipping {fullname}")
1080
- return None
1081
-
1082
- try:
1083
- logger.debug(f"[HOOK] Looking for spec: {fullname}")
1084
- spec = importlib.util.find_spec(fullname)
1085
- if spec is not None:
1086
- logger.debug(f"[HOOK] Spec found, trying normal import: {fullname}")
1087
- _importing.active = True
1088
- try:
1089
- __import__(fullname)
1090
- logger.info(f"✓ [HOOK] Module {fullname} imported successfully, no wrapping needed")
1091
- return None
1092
- finally:
1093
- _importing.active = False
1094
- except ImportError as e:
1095
- logger.info(f"⚠ [HOOK] Module {fullname} has missing dependencies, wrapping: {e}")
1096
- wrapped_spec = self._wrap_serialization_module(fullname)
1097
- if wrapped_spec is not None:
1098
- logger.info(f"✓ [HOOK] Successfully wrapped: {fullname}")
1099
- return wrapped_spec
1100
- logger.warning(f"✗ [HOOK] Failed to wrap: {fullname}")
1101
- except (ModuleNotFoundError,) as e:
1102
- logger.debug(f"[HOOK] Module {fullname} not found, skipping wrap: {e}")
1103
- pass
1104
- except Exception as e:
1105
- logger.warning(f"[HOOK] Error checking module {fullname}: {e}")
1106
- finally:
1107
- _mark_import_finished(fullname)
1108
-
1109
- return None
1110
-
1111
- # Only handle top-level packages
1112
- if '.' in fullname:
1113
- return None
1114
-
1115
- _mark_import_started(fullname)
1116
- try:
1117
- try:
1118
- if not is_lazy_install_enabled(self._package_name):
1119
- return None
1120
-
1121
- module, success = lazy_import_with_install(
1122
- fullname,
1123
- installer_package=self._package_name
1124
- )
1125
-
1126
- if success and module:
1127
- return importlib.util.find_spec(fullname)
1128
-
1129
- except Exception as e:
1130
- logger.debug(f"Lazy import hook failed for {fullname}: {e}")
1131
-
1132
- return None
1133
- finally:
1134
- _mark_import_finished(fullname)
1135
-
1136
- def _wrap_serialization_module(self, fullname: str):
1137
- """Wrap serialization module loading to defer missing dependencies."""
1138
- logger.info(f"[STAGE 1] Starting wrap of module: {fullname}")
1139
-
1140
- try:
1141
- logger.debug(f"[STAGE 1] Getting spec for: {fullname}")
1142
- spec = importlib.util.find_spec(fullname)
1143
- if not spec or not spec.loader:
1144
- logger.warning(f"[STAGE 1] No spec or loader for: {fullname}")
1145
- return None
1146
-
1147
- logger.debug(f"[STAGE 1] Creating module from spec: {fullname}")
1148
- module = importlib.util.module_from_spec(spec)
1149
-
1150
- deferred_imports = {}
1151
-
1152
- logger.debug(f"[STAGE 1] Setting up import wrapper for: {fullname}")
1153
- original_import = builtins.__import__
1154
-
1155
- def capture_import_errors(name, *args, **kwargs):
1156
- """Intercept imports and defer ONLY external missing packages."""
1157
- logger.debug(f"[STAGE 1] capture_import_errors: Trying to import '{name}' in {fullname}")
1158
-
1159
- if _is_import_in_progress(name):
1160
- logger.debug(f"[STAGE 1] Import '{name}' already in progress, using original_import")
1161
- return original_import(name, *args, **kwargs)
1162
-
1163
- _mark_import_started(name)
1164
- try:
1165
- result = original_import(name, *args, **kwargs)
1166
- logger.debug(f"[STAGE 1] ✓ Successfully imported '{name}'")
1167
- return result
1168
- except ImportError as e:
1169
- logger.debug(f"[STAGE 1] ✗ Import failed for '{name}': {e}")
1170
-
1171
- if name.startswith('exonware.') or name.startswith('xwsystem.'):
1172
- logger.info(f"[STAGE 1] Letting internal import '{name}' fail normally (internal package)")
1173
- raise
1174
-
1175
- if '.' in name:
1176
- logger.info(f"[STAGE 1] Letting submodule '{name}' fail normally (has dots)")
1177
- raise
1178
-
1179
- logger.info(f"⏳ [STAGE 1] DEFERRING missing external package '{name}' in {fullname}")
1180
- deferred = DeferredImportError(name, e, self._package_name)
1181
- deferred_imports[name] = deferred
1182
- return deferred
1183
- finally:
1184
- _mark_import_finished(name)
1185
-
1186
- logger.debug(f"[STAGE 1] Executing module with import wrapper: {fullname}")
1187
- builtins.__import__ = capture_import_errors
1188
- try:
1189
- spec.loader.exec_module(module)
1190
- logger.debug(f"[STAGE 1] Module execution completed: {fullname}")
1191
-
1192
- if deferred_imports:
1193
- logger.info(f"✓ [STAGE 1] Module {fullname} loaded with {len(deferred_imports)} deferred imports: {list(deferred_imports.keys())}")
1194
- # Replace None values with deferred import proxies (for modules that catch ImportError and set to None)
1195
- self._replace_none_with_deferred(module, deferred_imports)
1196
- self._wrap_module_classes(module, deferred_imports)
1197
- else:
1198
- logger.info(f"✓ [STAGE 1] Module {fullname} loaded with NO deferred imports (all dependencies available)")
1199
-
1200
- finally:
1201
- logger.debug(f"[STAGE 1] Restoring original __import__")
1202
- builtins.__import__ = original_import
1203
-
1204
- logger.debug(f"[STAGE 1] Registering module in sys.modules: {fullname}")
1205
- sys.modules[fullname] = module
1206
- logger.info(f"✓ [STAGE 1] Successfully wrapped and registered: {fullname}")
1207
- return spec
1208
-
1209
- except Exception as e:
1210
- logger.debug(f"Could not wrap {fullname}: {e}")
1211
- return None
1212
-
1213
- def _replace_none_with_deferred(self, module, deferred_imports: Dict):
1214
- """
1215
- Replace None values in module namespace with deferred import proxies.
1216
-
1217
- Some modules catch ImportError and set the variable to None (e.g., yaml = None).
1218
- This method replaces those None values with DeferredImportError proxies so the
1219
- hook can install missing packages when the variable is accessed.
1220
- """
1221
- logger.debug(f"[STAGE 1] Replacing None with deferred imports in {module.__name__}")
1222
- replaced_count = 0
1223
-
1224
- for dep_name, deferred_import in deferred_imports.items():
1225
- # Check if module has this variable set to None
1226
- if hasattr(module, dep_name):
1227
- current_value = getattr(module, dep_name)
1228
- if current_value is None:
1229
- logger.info(f"[STAGE 1] Replacing {dep_name}=None with deferred import proxy in {module.__name__}")
1230
- setattr(module, dep_name, deferred_import)
1231
- replaced_count += 1
1232
-
1233
- if replaced_count > 0:
1234
- logger.info(f"✓ [STAGE 1] Replaced {replaced_count} None values with deferred imports in {module.__name__}")
1235
-
1236
- def _wrap_module_classes(self, module, deferred_imports: Dict):
1237
- """Wrap classes in a module that depend on deferred imports."""
1238
- logger.debug(f"[STAGE 1] Wrapping classes in {module.__name__} (deferred: {list(deferred_imports.keys())})")
1239
- wrapped_count = 0
1240
-
1241
- for name, obj in list(module.__dict__.items()):
1242
- if isinstance(obj, type):
1243
- for dep_name in deferred_imports.keys():
1244
- module_file = getattr(module, '__file__', '')
1245
- if dep_name.lower() in name.lower() or dep_name.lower() in module_file.lower():
1246
- logger.debug(f"[STAGE 1] Class '{name}' depends on '{dep_name}', wrapping...")
1247
- wrapped = self._create_lazy_class_wrapper(obj, deferred_imports[dep_name])
1248
- module.__dict__[name] = wrapped
1249
- wrapped_count += 1
1250
- logger.info(f"✓ [STAGE 1] Wrapped class '{name}' in {module.__name__}")
1251
- break
1252
-
1253
- logger.info(f"[STAGE 1] Wrapped {wrapped_count} classes in {module.__name__}")
1254
-
1255
- def _create_lazy_class_wrapper(self, original_class, deferred_import: DeferredImportError):
1256
- """Create a wrapper class that installs dependencies when instantiated."""
1257
- class LazyClassWrapper:
1258
- """Lazy wrapper that installs dependencies on first instantiation."""
1259
-
1260
- def __init__(self, *args, **kwargs):
1261
- """Install dependency and create real instance."""
1262
- deferred_import._try_install_and_import()
1263
-
1264
- real_module = importlib.reload(sys.modules[original_class.__module__])
1265
- real_class = getattr(real_module, original_class.__name__)
1266
-
1267
- real_instance = real_class(*args, **kwargs)
1268
- self.__class__ = real_class
1269
- self.__dict__ = real_instance.__dict__
1270
-
1271
- def __repr__(self):
1272
- return f"<Lazy{original_class.__name__}: will install dependencies on init>"
1273
-
1274
- LazyClassWrapper.__name__ = f"Lazy{original_class.__name__}"
1275
- LazyClassWrapper.__qualname__ = f"Lazy{original_class.__qualname__}"
1276
- LazyClassWrapper.__module__ = original_class.__module__
1277
- LazyClassWrapper.__doc__ = original_class.__doc__
1278
-
1279
- return LazyClassWrapper
1280
-
1281
-
1282
- # Registry of installed hooks per package
1283
- _installed_hooks: Dict[str, LazyMetaPathFinder] = {}
1284
- _hook_lock = threading.RLock()
1285
-
1286
-
1287
- def install_import_hook(package_name: str = 'default') -> None:
1288
- """Install performant import hook for automatic lazy installation."""
1289
- global _installed_hooks
1290
-
1291
- logger.info(f"[HOOK INSTALL] Installing import hook for package: {package_name}")
1292
-
1293
- with _hook_lock:
1294
- if package_name in _installed_hooks:
1295
- logger.info(f"[HOOK INSTALL] Import hook already installed for {package_name}")
1296
- return
1297
-
1298
- logger.debug(f"[HOOK INSTALL] Creating LazyMetaPathFinder for {package_name}")
1299
- hook = LazyMetaPathFinder(package_name)
1300
-
1301
- logger.debug(f"[HOOK INSTALL] Current sys.meta_path has {len(sys.meta_path)} entries")
1302
- sys.meta_path.insert(0, hook)
1303
- _installed_hooks[package_name] = hook
1304
-
1305
- logger.info(f"✅ [HOOK INSTALL] Lazy import hook installed for {package_name} (now {len(sys.meta_path)} meta_path entries)")
1306
-
1307
-
1308
- def uninstall_import_hook(package_name: str = 'default') -> None:
1309
- """Uninstall import hook for a package."""
1310
- global _installed_hooks
1311
-
1312
- with _hook_lock:
1313
- if package_name in _installed_hooks:
1314
- hook = _installed_hooks[package_name]
1315
- try:
1316
- sys.meta_path.remove(hook)
1317
- except ValueError:
1318
- pass
1319
- del _installed_hooks[package_name]
1320
- logger.info(f"Lazy import hook uninstalled for {package_name}")
1321
-
1322
-
1323
- def is_import_hook_installed(package_name: str = 'default') -> bool:
1324
- """Check if import hook is installed for a package."""
1325
- return package_name in _installed_hooks
1326
-
1327
-
1328
- # =============================================================================
1329
- # SECTION 4: LAZY LOADING & CACHING (~300 lines)
1330
- # =============================================================================
1331
-
1332
- class LazyLoader(ALazyLoader):
1333
- """
1334
- Thread-safe lazy loader for modules with caching.
1335
- Implements Proxy pattern for deferred module loading.
1336
- """
1337
-
1338
- def load_module(self, module_path: str = None) -> ModuleType:
1339
- """Thread-safe module loading with caching."""
1340
- if module_path is None:
1341
- module_path = self._module_path
1342
-
1343
- if self._cached_module is not None:
1344
- return self._cached_module
1345
-
1346
- with self._lock:
1347
- if self._cached_module is not None:
1348
- return self._cached_module
1349
-
1350
- if self._loading:
1351
- raise ImportError(f"Circular import detected for {module_path}")
1352
-
1353
- try:
1354
- self._loading = True
1355
- logger.debug(f"Lazy loading module: {module_path}")
1356
-
1357
- self._cached_module = importlib.import_module(module_path)
1358
-
1359
- logger.debug(f"Successfully loaded: {module_path}")
1360
- return self._cached_module
1361
-
1362
- except Exception as e:
1363
- logger.error(f"Failed to load module {module_path}: {e}")
1364
- raise ImportError(f"Failed to load {module_path}: {e}") from e
1365
- finally:
1366
- self._loading = False
1367
-
1368
- def unload_module(self, module_path: str) -> None:
1369
- """Unload a module from cache."""
1370
- with self._lock:
1371
- if module_path == self._module_path:
1372
- self._cached_module = None
1373
-
1374
- def __getattr__(self, name: str) -> Any:
1375
- """Get attribute from lazily loaded module."""
1376
- module = self.load_module()
1377
- try:
1378
- return getattr(module, name)
1379
- except AttributeError:
1380
- raise AttributeError(
1381
- f"module '{self._module_path}' has no attribute '{name}'"
1382
- )
1383
-
1384
- def __dir__(self) -> list:
1385
- """Return available attributes from loaded module."""
1386
- module = self.load_module()
1387
- return dir(module)
1388
-
1389
-
1390
- class LazyImporter:
1391
- """
1392
- Lazy importer that defers heavy module imports until first access.
1393
- """
1394
-
1395
- __slots__ = ('_enabled', '_lazy_modules', '_loaded_modules', '_lock', '_access_counts')
1396
-
1397
- def __init__(self):
1398
- """Initialize lazy importer."""
1399
- self._enabled = False
1400
- self._lazy_modules: Dict[str, str] = {}
1401
- self._loaded_modules: Dict[str, ModuleType] = {}
1402
- self._access_counts: Dict[str, int] = {}
1403
- self._lock = threading.RLock()
1404
-
1405
- def enable(self) -> None:
1406
- """Enable lazy imports."""
1407
- with self._lock:
1408
- self._enabled = True
1409
- logger.info("Lazy imports enabled")
1410
-
1411
- def disable(self) -> None:
1412
- """Disable lazy imports."""
1413
- with self._lock:
1414
- self._enabled = False
1415
- logger.info("Lazy imports disabled")
1416
-
1417
- def is_enabled(self) -> bool:
1418
- """Check if lazy imports are enabled."""
1419
- return self._enabled
1420
-
1421
- def register_lazy_module(self, module_name: str, module_path: str = None) -> None:
1422
- """Register a module for lazy loading."""
1423
- with self._lock:
1424
- if module_path is None:
1425
- module_path = module_name
1426
-
1427
- self._lazy_modules[module_name] = module_path
1428
- self._access_counts[module_name] = 0
1429
- logger.debug(f"Registered lazy module: {module_name} -> {module_path}")
1430
-
1431
- def import_module(self, module_name: str, package_name: str = None) -> Any:
1432
- """Import a module with lazy loading."""
1433
- with self._lock:
1434
- if not self._enabled:
1435
- return importlib.import_module(module_name)
1436
-
1437
- if module_name in self._loaded_modules:
1438
- self._access_counts[module_name] += 1
1439
- return self._loaded_modules[module_name]
1440
-
1441
- if module_name in self._lazy_modules:
1442
- module_path = self._lazy_modules[module_name]
1443
-
1444
- try:
1445
- actual_module = importlib.import_module(module_path)
1446
- self._loaded_modules[module_name] = actual_module
1447
- self._access_counts[module_name] += 1
1448
-
1449
- logger.debug(f"Lazy loaded module: {module_name}")
1450
- return actual_module
1451
-
1452
- except ImportError as e:
1453
- logger.error(f"Failed to lazy load {module_name}: {e}")
1454
- raise
1455
- else:
1456
- return importlib.import_module(module_name)
1457
-
1458
- def preload_module(self, module_name: str) -> bool:
1459
- """Preload a registered lazy module."""
1460
- with self._lock:
1461
- if module_name not in self._lazy_modules:
1462
- logger.warning(f"Module {module_name} not registered for lazy loading")
1463
- return False
1464
-
1465
- try:
1466
- self.import_module(module_name)
1467
- logger.info(f"Preloaded module: {module_name}")
1468
- return True
1469
- except Exception as e:
1470
- logger.error(f"Failed to preload {module_name}: {e}")
1471
- return False
1472
-
1473
- def get_stats(self) -> Dict[str, Any]:
1474
- """Get lazy import statistics."""
1475
- with self._lock:
1476
- return {
1477
- 'enabled': self._enabled,
1478
- 'registered_modules': list(self._lazy_modules.keys()),
1479
- 'loaded_modules': list(self._loaded_modules.keys()),
1480
- 'access_counts': self._access_counts.copy(),
1481
- 'total_registered': len(self._lazy_modules),
1482
- 'total_loaded': len(self._loaded_modules)
1483
- }
1484
-
1485
-
1486
- class LazyModuleRegistry:
1487
- """
1488
- Registry for managing lazy-loaded modules with performance tracking.
1489
- """
1490
-
1491
- __slots__ = ('_modules', '_load_times', '_lock', '_access_counts')
1492
-
1493
- def __init__(self):
1494
- """Initialize the registry."""
1495
- self._modules: Dict[str, LazyLoader] = {}
1496
- self._load_times: Dict[str, float] = {}
1497
- self._access_counts: Dict[str, int] = {}
1498
- self._lock = threading.RLock()
1499
-
1500
- def register_module(self, name: str, module_path: str) -> None:
1501
- """Register a module for lazy loading."""
1502
- with self._lock:
1503
- if name in self._modules:
1504
- logger.warning(f"Module '{name}' already registered, overwriting")
1505
-
1506
- self._modules[name] = LazyLoader(module_path)
1507
- self._access_counts[name] = 0
1508
- logger.debug(f"Registered lazy module: {name} -> {module_path}")
1509
-
1510
- def get_module(self, name: str) -> LazyLoader:
1511
- """Get a lazy-loaded module."""
1512
- with self._lock:
1513
- if name not in self._modules:
1514
- raise KeyError(f"Module '{name}' not registered")
1515
-
1516
- self._access_counts[name] += 1
1517
- return self._modules[name]
1518
-
1519
- def preload_frequently_used(self, threshold: int = 5) -> None:
1520
- """Preload modules that are accessed frequently."""
1521
- with self._lock:
1522
- for name, count in self._access_counts.items():
1523
- if count >= threshold:
1524
- try:
1525
- _ = self._modules[name].load_module()
1526
- logger.info(f"Preloaded frequently used module: {name}")
1527
- except Exception as e:
1528
- logger.warning(f"Failed to preload {name}: {e}")
1529
-
1530
- def get_stats(self) -> Dict[str, Any]:
1531
- """Get loading statistics."""
1532
- with self._lock:
1533
- loaded_count = sum(
1534
- 1 for loader in self._modules.values()
1535
- if loader.is_loaded()
1536
- )
1537
-
1538
- return {
1539
- 'total_registered': len(self._modules),
1540
- 'loaded_count': loaded_count,
1541
- 'unloaded_count': len(self._modules) - loaded_count,
1542
- 'access_counts': self._access_counts.copy(),
1543
- 'load_times': self._load_times.copy(),
1544
- }
1545
-
1546
- def clear_cache(self) -> None:
1547
- """Clear all cached modules."""
1548
- with self._lock:
1549
- for name, loader in self._modules.items():
1550
- loader.unload_module(loader._module_path)
1551
- logger.info("Cleared all cached modules")
1552
-
1553
-
1554
- class LazyPerformanceMonitor:
1555
- """Performance monitor for lazy loading operations."""
1556
-
1557
- __slots__ = ('_load_times', '_access_counts', '_memory_usage')
1558
-
1559
- def __init__(self):
1560
- """Initialize performance monitor."""
1561
- self._load_times = {}
1562
- self._access_counts = {}
1563
- self._memory_usage = {}
1564
-
1565
- def record_load_time(self, module: str, load_time: float) -> None:
1566
- """Record module load time."""
1567
- self._load_times[module] = load_time
1568
-
1569
- def record_access(self, module: str) -> None:
1570
- """Record module access."""
1571
- self._access_counts[module] = self._access_counts.get(module, 0) + 1
1572
-
1573
- def get_stats(self) -> Dict[str, Any]:
1574
- """Get performance statistics."""
1575
- return {
1576
- 'load_times': self._load_times.copy(),
1577
- 'access_counts': self._access_counts.copy(),
1578
- 'memory_usage': self._memory_usage.copy()
1579
- }
1580
-
1581
-
1582
- # Global instances
1583
- _lazy_importer = LazyImporter()
1584
- _global_registry = LazyModuleRegistry()
1585
-
1586
-
1587
- _lazy_importer = LazyImporter()
1588
- _global_registry = LazyModuleRegistry()
1589
-
1590
-
1591
- def enable_lazy_imports() -> None:
1592
- """Enable lazy imports (loader only)."""
1593
- _lazy_importer.enable()
1594
-
1595
-
1596
- def disable_lazy_imports() -> None:
1597
- """Disable lazy imports (loader only)."""
1598
- _lazy_importer.disable()
1599
-
1600
-
1601
- def is_lazy_import_enabled() -> bool:
1602
- """Check if lazy imports are enabled."""
1603
- return _lazy_importer.is_enabled()
1604
-
1605
-
1606
- def lazy_import(module_name: str, package_name: str = None) -> Any:
1607
- """Import a module with lazy loading."""
1608
- return _lazy_importer.import_module(module_name, package_name)
1609
-
1610
-
1611
- def register_lazy_module(module_name: str, module_path: str = None) -> None:
1612
- """Register a module for lazy loading."""
1613
- _lazy_importer.register_lazy_module(module_name, module_path)
1614
- _global_registry.register_module(module_name, module_path or module_name)
1615
-
1616
-
1617
- def preload_module(module_name: str) -> bool:
1618
- """Preload a registered lazy module."""
1619
- return _lazy_importer.preload_module(module_name)
1620
-
1621
-
1622
- def get_lazy_module(name: str) -> LazyLoader:
1623
- """Get a lazy-loaded module from the global registry."""
1624
- return _global_registry.get_module(name)
1625
-
1626
-
1627
- def get_loading_stats() -> Dict[str, Any]:
1628
- """Get loading statistics from the global registry."""
1629
- return _global_registry.get_stats()
1630
-
1631
-
1632
- def preload_frequently_used(threshold: int = 5) -> None:
1633
- """Preload frequently used modules from the global registry."""
1634
- _global_registry.preload_frequently_used(threshold)
1635
-
1636
-
1637
- def get_lazy_import_stats() -> Dict[str, Any]:
1638
- """Get lazy import statistics."""
1639
- return _lazy_importer.get_stats()
1640
-
1641
-
1642
- # =============================================================================
1643
- # SECTION 5: CONFIGURATION & REGISTRY (~200 lines)
1644
- # =============================================================================
1645
-
1646
- # Performance optimization: Cache detection results per package
1647
- _lazy_detection_cache: Dict[str, bool] = {}
1648
- _lazy_detection_lock = threading.RLock()
1649
-
1650
- # Performance optimization: Module-level constant for mode enum conversion
1651
- _MODE_ENUM_MAP = {
1652
- "auto": LazyInstallMode.AUTO,
1653
- "interactive": LazyInstallMode.INTERACTIVE,
1654
- "warn": LazyInstallMode.WARN,
1655
- "disabled": LazyInstallMode.DISABLED,
1656
- "dry_run": LazyInstallMode.DRY_RUN,
1657
- }
1658
-
1659
-
1660
- def _lazy_env_override(package_name: str) -> Optional[bool]:
1661
- env_var = f"{package_name.upper()}_LAZY_INSTALL"
1662
- raw_value = os.environ.get(env_var)
1663
- if raw_value is None:
1664
- return None
1665
-
1666
- normalized = raw_value.strip().lower()
1667
- if normalized in ("true", "1", "yes", "on"):
1668
- return True
1669
- if normalized in ("false", "0", "no", "off"):
1670
- return False
1671
- return None
1672
-
1673
-
1674
- def _lazy_marker_installed() -> bool:
1675
- if sys.version_info < (3, 8):
1676
- return False
1677
-
1678
- try:
1679
- from importlib import metadata
1680
- except Exception as exc:
1681
- logger.debug(f"importlib.metadata unavailable for lazy detection: {exc}")
1682
- return False
1683
-
1684
- try:
1685
- metadata.distribution("exonware-xwlazy")
1686
- logger.info("✅ Detected exonware-xwlazy marker package")
1687
- return True
1688
- except metadata.PackageNotFoundError:
1689
- logger.info("❌ exonware-xwlazy marker package not installed")
1690
- return False
1691
- except Exception as exc:
1692
- logger.debug(f"Failed to inspect marker package: {exc}")
1693
- return False
1694
-
1695
-
1696
- def _detect_lazy_installation(package_name: str) -> bool:
1697
- with _lazy_detection_lock:
1698
- cached = _lazy_detection_cache.get(package_name)
1699
- if cached is not None:
1700
- return cached
1701
-
1702
- env_override = _lazy_env_override(package_name)
1703
- if env_override is not None:
1704
- with _lazy_detection_lock:
1705
- _lazy_detection_cache[package_name] = env_override
1706
- return env_override
1707
-
1708
- state_manager = LazyStateManager(package_name)
1709
- manual_state = state_manager.get_manual_state()
1710
- if manual_state is not None:
1711
- with _lazy_detection_lock:
1712
- _lazy_detection_cache[package_name] = manual_state
1713
- return manual_state
1714
-
1715
- cached_state = state_manager.get_cached_auto_state()
1716
- if cached_state is not None:
1717
- with _lazy_detection_lock:
1718
- _lazy_detection_cache[package_name] = cached_state
1719
- return cached_state
1720
-
1721
- detected = _lazy_marker_installed()
1722
- state_manager.set_auto_state(detected)
1723
-
1724
- with _lazy_detection_lock:
1725
- _lazy_detection_cache[package_name] = detected
1726
-
1727
- return detected
1728
-
1729
-
1730
- class LazyInstallConfig:
1731
- """Global configuration for lazy installation per package."""
1732
- _configs: Dict[str, bool] = {}
1733
- _modes: Dict[str, str] = {}
1734
- _initialized: Dict[str, bool] = {}
1735
- _manual_overrides: Dict[str, bool] = {}
1736
-
1737
- @classmethod
1738
- def set(
1739
- cls,
1740
- package_name: str,
1741
- enabled: bool,
1742
- mode: str = "auto",
1743
- install_hook: bool = True,
1744
- manual: bool = False,
1745
- ) -> None:
1746
- """Enable or disable lazy installation for a specific package."""
1747
- package_key = package_name.lower()
1748
- state_manager = LazyStateManager(package_name)
1749
-
1750
- if manual:
1751
- cls._manual_overrides[package_key] = True
1752
- state_manager.set_manual_state(enabled)
1753
- elif cls._manual_overrides.get(package_key):
1754
- logger.debug(
1755
- f"Lazy install config for {package_key} already overridden manually; skipping auto configuration."
1756
- )
1757
- return
1758
- else:
1759
- state_manager.set_manual_state(None)
1760
-
1761
- cls._configs[package_key] = enabled
1762
- cls._modes[package_key] = mode
1763
-
1764
- cls._initialize_package(package_key, enabled, mode, install_hook=install_hook)
1765
-
1766
- @classmethod
1767
- def _initialize_package(cls, package_key: str, enabled: bool, mode: str, install_hook: bool = True) -> None:
1768
- """Initialize lazy installation for a specific package."""
1769
- if enabled:
1770
- try:
1771
- enable_lazy_install(package_key)
1772
-
1773
- mode_enum = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.AUTO)
1774
- set_lazy_install_mode(package_key, mode_enum)
1775
-
1776
- if install_hook:
1777
- if not is_import_hook_installed(package_key):
1778
- install_import_hook(package_key)
1779
- logger.info(f"✅ Lazy installation initialized for {package_key} (mode: {mode}, hook: installed)")
1780
- else:
1781
- uninstall_import_hook(package_key)
1782
- logger.info(f"✅ Lazy installation initialized for {package_key} (mode: {mode}, hook: disabled)")
1783
-
1784
- cls._initialized[package_key] = True
1785
- except ImportError as e:
1786
- logger.warning(f"⚠️ Could not enable lazy install for {package_key}: {e}")
1787
- else:
1788
- try:
1789
- disable_lazy_install(package_key)
1790
- except ImportError:
1791
- pass
1792
- uninstall_import_hook(package_key)
1793
- cls._initialized[package_key] = False
1794
- logger.info(f"❌ Lazy installation disabled for {package_key}")
1795
-
1796
- @classmethod
1797
- def is_enabled(cls, package_name: str) -> bool:
1798
- """Check if lazy installation is enabled for a package."""
1799
- return cls._configs.get(package_name.lower(), False)
1800
-
1801
- @classmethod
1802
- def get_mode(cls, package_name: str) -> str:
1803
- """Get the lazy installation mode for a package."""
1804
- return cls._modes.get(package_name.lower(), "auto")
1805
-
1806
-
1807
- def config_package_lazy_install_enabled(
1808
- package_name: str,
1809
- enabled: bool = None,
1810
- mode: str = "auto",
1811
- install_hook: bool = True
1812
- ) -> None:
1813
- """
1814
- Simple one-line configuration for package lazy installation.
1815
-
1816
- Args:
1817
- package_name: Package name (e.g., "xwsystem", "xwnode", "xwdata")
1818
- enabled: True to enable, False to disable, None to auto-detect from pip installation
1819
- mode: Installation mode - "auto", "interactive", "disabled", "dry_run"
1820
- install_hook: Whether to install the import hook (default: True)
1821
-
1822
- Examples:
1823
- # Auto-detect from installation
1824
- config_package_lazy_install_enabled("xwsystem")
1825
-
1826
- # Force enable
1827
- config_package_lazy_install_enabled("xwnode", True, "interactive")
1828
-
1829
- # Force disable
1830
- config_package_lazy_install_enabled("xwdata", False)
1831
- """
1832
- manual_override = enabled is not None
1833
- if enabled is None:
1834
- enabled = _detect_lazy_installation(package_name)
1835
-
1836
- LazyInstallConfig.set(
1837
- package_name,
1838
- enabled,
1839
- mode,
1840
- install_hook=install_hook,
1841
- manual=manual_override,
1842
- )
1843
-
1844
-
1845
- # =============================================================================
1846
- # SECTION 6: FACADE - UNIFIED API (~150 lines)
1847
- # =============================================================================
1848
-
1849
- class LazyModeFacade:
1850
- """
1851
- Main facade for lazy mode operations in xwsystem.
1852
- Provides a unified interface for lazy loading functionality.
1853
- """
1854
-
1855
- __slots__ = ('_enabled', '_strategy', '_config', '_performance_monitor')
1856
-
1857
- def __init__(self):
1858
- """Initialize lazy mode facade."""
1859
- self._enabled = False
1860
- self._strategy = None
1861
- self._config = {}
1862
- self._performance_monitor = None
1863
-
1864
- def enable(self, strategy: str = "on_demand", **kwargs) -> None:
1865
- """Enable lazy mode with specified strategy."""
1866
- self._enabled = True
1867
- self._strategy = strategy
1868
-
1869
- package_name = kwargs.pop('package_name', 'xwsystem').lower()
1870
- enable_lazy_import_flag = kwargs.pop('enable_lazy_imports', True)
1871
- enable_lazy_install_flag = kwargs.pop('enable_lazy_install', True)
1872
- lazy_install_mode = kwargs.pop('lazy_install_mode', "auto")
1873
- install_hook = kwargs.pop('install_hook', True)
1874
-
1875
- self._config.update({
1876
- 'package_name': package_name,
1877
- 'enable_lazy_imports': enable_lazy_import_flag,
1878
- 'enable_lazy_install': enable_lazy_install_flag,
1879
- 'lazy_install_mode': lazy_install_mode,
1880
- 'install_hook': install_hook,
1881
- })
1882
- self._config.update(kwargs)
1883
-
1884
- logger.info(f"Lazy mode enabled with strategy: {strategy}")
1885
-
1886
- if enable_lazy_import_flag:
1887
- _lazy_importer.enable()
1888
- else:
1889
- _lazy_importer.disable()
1890
-
1891
- if enable_lazy_install_flag:
1892
- config_package_lazy_install_enabled(
1893
- package_name,
1894
- True,
1895
- lazy_install_mode,
1896
- install_hook=install_hook,
1897
- )
1898
- else:
1899
- config_package_lazy_install_enabled(
1900
- package_name,
1901
- False,
1902
- install_hook=install_hook,
1903
- )
1904
- uninstall_import_hook(package_name)
1905
-
1906
- if self._config.get('enable_monitoring', True):
1907
- self._performance_monitor = LazyPerformanceMonitor()
1908
-
1909
- def disable(self) -> None:
1910
- """Disable lazy mode and cleanup resources."""
1911
- self._enabled = False
1912
- self._strategy = None
1913
-
1914
- package_name = self._config.get('package_name', 'xwsystem')
1915
-
1916
- if self._config.get('enable_lazy_imports', True):
1917
- _lazy_importer.disable()
1918
-
1919
- if self._config.get('enable_lazy_install', True):
1920
- LazyInstallConfig.set(
1921
- package_name,
1922
- False,
1923
- self._config.get('lazy_install_mode', 'auto'),
1924
- install_hook=self._config.get('install_hook', True),
1925
- )
1926
-
1927
- if self._config.get('clear_cache_on_disable', True):
1928
- _global_registry.clear_cache()
1929
-
1930
- self._performance_monitor = None
1931
-
1932
- logger.info("Lazy mode disabled")
1933
-
1934
- def is_enabled(self) -> bool:
1935
- """Check if lazy mode is currently enabled."""
1936
- return self._enabled
1937
-
1938
- def get_stats(self) -> Dict[str, Any]:
1939
- """Get lazy mode performance statistics."""
1940
- stats = _global_registry.get_stats()
1941
- stats.update({
1942
- 'enabled': self._enabled,
1943
- 'strategy': self._strategy,
1944
- 'config': self._config.copy()
1945
- })
1946
-
1947
- if self._performance_monitor:
1948
- stats['performance'] = self._performance_monitor.get_stats()
1949
-
1950
- return stats
1951
-
1952
- def configure(self, **kwargs) -> None:
1953
- """Configure lazy mode settings."""
1954
- self._config.update(kwargs)
1955
- logger.debug(f"Lazy mode configuration updated: {kwargs}")
1956
-
1957
- def preload(self, modules: List[str]) -> None:
1958
- """Preload specified modules."""
1959
- for module_name in modules:
1960
- try:
1961
- loader = _global_registry.get_module(module_name)
1962
- _ = loader.load_module()
1963
- logger.info(f"Preloaded module: {module_name}")
1964
- except KeyError:
1965
- logger.warning(f"Module not registered: {module_name}")
1966
- except Exception as e:
1967
- logger.error(f"Failed to preload {module_name}: {e}")
1968
-
1969
- def optimize(self) -> None:
1970
- """Run optimization based on current usage patterns."""
1971
- if not self._enabled:
1972
- return
1973
-
1974
- threshold = self._config.get('preload_threshold', 5)
1975
- _global_registry.preload_frequently_used(threshold)
1976
-
1977
- logger.info("Lazy mode optimization completed")
1978
-
1979
-
1980
- # Global lazy mode facade instance
1981
- _lazy_facade = LazyModeFacade()
1982
-
1983
-
1984
- def enable_lazy_mode(strategy: str = "on_demand", **kwargs) -> None:
1985
- """Enable lazy mode with specified strategy."""
1986
- _lazy_facade.enable(strategy, **kwargs)
1987
-
1988
-
1989
- def disable_lazy_mode() -> None:
1990
- """Disable lazy mode and cleanup resources."""
1991
- _lazy_facade.disable()
1992
-
1993
-
1994
- def is_lazy_mode_enabled() -> bool:
1995
- """Check if lazy mode is currently enabled."""
1996
- return _lazy_facade.is_enabled()
1997
-
1998
-
1999
- def get_lazy_mode_stats() -> Dict[str, Any]:
2000
- """Get lazy mode performance statistics."""
2001
- return _lazy_facade.get_stats()
2002
-
2003
-
2004
- def configure_lazy_mode(**kwargs) -> None:
2005
- """Configure lazy mode settings."""
2006
- _lazy_facade.configure(**kwargs)
2007
-
2008
-
2009
- def preload_modules(modules: List[str]) -> None:
2010
- """Preload specified modules."""
2011
- _lazy_facade.preload(modules)
2012
-
2013
-
2014
- def optimize_lazy_mode() -> None:
2015
- """Run optimization based on current usage patterns."""
2016
- _lazy_facade.optimize()
2017
-
2018
-
2019
- # =============================================================================
2020
- # SECTION 7: PUBLIC API - SIMPLE FUNCTIONS (~200 lines)
2021
- # =============================================================================
2022
-
2023
- def enable_lazy_install(package_name: str = 'default') -> None:
2024
- """Enable lazy installation for a specific package."""
2025
- installer = LazyInstallerRegistry.get_instance(package_name)
2026
- installer.enable()
2027
-
2028
-
2029
- def disable_lazy_install(package_name: str = 'default') -> None:
2030
- """Disable lazy installation for a specific package."""
2031
- installer = LazyInstallerRegistry.get_instance(package_name)
2032
- installer.disable()
2033
-
2034
-
2035
- def is_lazy_install_enabled(package_name: str = 'default') -> bool:
2036
- """Check if lazy installation is enabled for a specific package."""
2037
- installer = LazyInstallerRegistry.get_instance(package_name)
2038
- return installer.is_enabled()
2039
-
2040
-
2041
- def set_lazy_install_mode(package_name: str, mode: LazyInstallMode) -> None:
2042
- """Set the lazy installation mode for a specific package."""
2043
- installer = LazyInstallerRegistry.get_instance(package_name)
2044
- installer.set_mode(mode)
2045
-
2046
-
2047
- def get_lazy_install_mode(package_name: str = 'default') -> LazyInstallMode:
2048
- """Get the lazy installation mode for a specific package."""
2049
- installer = LazyInstallerRegistry.get_instance(package_name)
2050
- return installer.get_mode()
2051
-
2052
-
2053
- def install_missing_package(package_name: str, installer_package: str = 'default') -> bool:
2054
- """Install a missing package."""
2055
- installer = LazyInstallerRegistry.get_instance(installer_package)
2056
- return installer.install_package(package_name)
2057
-
2058
-
2059
- def install_and_import(
2060
- module_name: str,
2061
- package_name: str = None,
2062
- installer_package: str = 'default'
2063
- ) -> Tuple[Optional[ModuleType], bool]:
2064
- """Install package and import module."""
2065
- installer = LazyInstallerRegistry.get_instance(installer_package)
2066
- return installer.install_and_import(module_name, package_name)
2067
-
2068
-
2069
- def get_lazy_install_stats(package_name: str = 'default') -> Dict[str, Any]:
2070
- """Get lazy installation statistics for a specific package."""
2071
- installer = LazyInstallerRegistry.get_instance(package_name)
2072
- return installer.get_stats()
2073
-
2074
-
2075
- def get_all_lazy_install_stats() -> Dict[str, Dict[str, Any]]:
2076
- """Get lazy installation statistics for all packages."""
2077
- all_instances = LazyInstallerRegistry.get_all_instances()
2078
- return {name: inst.get_stats() for name, inst in all_instances.items()}
2079
-
2080
-
2081
- def lazy_import_with_install(
2082
- module_name: str,
2083
- package_name: str = None,
2084
- installer_package: str = 'default'
2085
- ) -> Tuple[Optional[ModuleType], bool]:
2086
- """
2087
- Lazy import with automatic installation.
2088
-
2089
- This function attempts to import a module, and if it fails due to ImportError,
2090
- it automatically installs the corresponding package using pip before retrying.
2091
- """
2092
- installer = LazyInstallerRegistry.get_instance(installer_package)
2093
- return installer.install_and_import(module_name, package_name)
2094
-
2095
-
2096
- def xwimport(
2097
- module_name: str,
2098
- package_name: str = None,
2099
- installer_package: str = 'default'
2100
- ) -> Any:
2101
- """
2102
- Simple lazy import with automatic installation.
2103
-
2104
- This function either returns the imported module or raises an ImportError.
2105
- """
2106
- module, available = lazy_import_with_install(module_name, package_name, installer_package)
2107
- if not available:
2108
- raise ImportError(f"Module {module_name} is not available and could not be installed")
2109
- return module
2110
-
2111
-
2112
- # Security & Policy APIs
2113
- def set_package_allow_list(package_name: str, allowed_packages: List[str]) -> None:
2114
- """Set allow list for a package (only these packages can be installed)."""
2115
- LazyInstallPolicy.set_allow_list(package_name, allowed_packages)
2116
-
2117
-
2118
- def set_package_deny_list(package_name: str, denied_packages: List[str]) -> None:
2119
- """Set deny list for a package (these packages cannot be installed)."""
2120
- LazyInstallPolicy.set_deny_list(package_name, denied_packages)
2121
-
2122
-
2123
- def add_to_package_allow_list(package_name: str, allowed_package: str) -> None:
2124
- """Add single package to allow list."""
2125
- LazyInstallPolicy.add_to_allow_list(package_name, allowed_package)
2126
-
2127
-
2128
- def add_to_package_deny_list(package_name: str, denied_package: str) -> None:
2129
- """Add single package to deny list."""
2130
- LazyInstallPolicy.add_to_deny_list(package_name, denied_package)
2131
-
2132
-
2133
- def set_package_index_url(package_name: str, index_url: str) -> None:
2134
- """Set PyPI index URL for a package."""
2135
- LazyInstallPolicy.set_index_url(package_name, index_url)
2136
-
2137
-
2138
- def set_package_extra_index_urls(package_name: str, urls: List[str]) -> None:
2139
- """Set extra index URLs for a package."""
2140
- LazyInstallPolicy.set_extra_index_urls(package_name, urls)
2141
-
2142
-
2143
- def add_package_trusted_host(package_name: str, host: str) -> None:
2144
- """Add trusted host for a package."""
2145
- LazyInstallPolicy.add_trusted_host(package_name, host)
2146
-
2147
-
2148
- def set_package_lockfile(package_name: str, lockfile_path: str) -> None:
2149
- """Set lockfile path for a package to track installed dependencies."""
2150
- LazyInstallPolicy.set_lockfile_path(package_name, lockfile_path)
2151
-
2152
-
2153
- def generate_package_sbom(package_name: str = 'default', output_path: str = None) -> Dict:
2154
- """Generate Software Bill of Materials (SBOM) for installed packages."""
2155
- installer = LazyInstallerRegistry.get_instance(package_name)
2156
- sbom = installer.generate_sbom()
2157
-
2158
- if output_path:
2159
- installer.export_sbom(output_path)
2160
-
2161
- return sbom
2162
-
2163
-
2164
- def check_externally_managed_environment() -> bool:
2165
- """Check if current Python environment is externally managed (PEP 668)."""
2166
- return _is_externally_managed()
2167
-
2168
-
2169
- # =============================================================================
2170
- # EXPORT ALL
2171
- # =============================================================================
2172
-
2173
- __all__ = [
2174
- # Core classes
2175
- 'DependencyMapper',
2176
- 'LazyDiscovery',
2177
- 'LazyInstaller',
2178
- 'LazyInstallPolicy',
2179
- 'LazyInstallerRegistry',
2180
- 'LazyImportHook',
2181
- 'LazyMetaPathFinder',
2182
- 'LazyLoader',
2183
- 'LazyImporter',
2184
- 'LazyModuleRegistry',
2185
- 'LazyPerformanceMonitor',
2186
- 'LazyInstallConfig',
2187
- 'LazyModeFacade',
2188
-
2189
- # Discovery functions
2190
- 'get_lazy_discovery',
2191
- 'discover_dependencies',
2192
- 'export_dependency_mappings',
2193
-
2194
- # Install functions
2195
- 'enable_lazy_install',
2196
- 'disable_lazy_install',
2197
- 'is_lazy_install_enabled',
2198
- 'set_lazy_install_mode',
2199
- 'get_lazy_install_mode',
2200
- 'install_missing_package',
2201
- 'install_and_import',
2202
- 'get_lazy_install_stats',
2203
- 'get_all_lazy_install_stats',
2204
- 'lazy_import_with_install',
2205
- 'xwimport',
2206
-
2207
- # Hook functions
2208
- 'install_import_hook',
2209
- 'uninstall_import_hook',
2210
- 'is_import_hook_installed',
2211
-
2212
- # Lazy loading functions
2213
- 'enable_lazy_imports',
2214
- 'disable_lazy_imports',
2215
- 'is_lazy_import_enabled',
2216
- 'lazy_import',
2217
- 'register_lazy_module',
2218
- 'preload_module',
2219
- 'get_lazy_module',
2220
- 'get_loading_stats',
2221
- 'preload_frequently_used',
2222
- 'get_lazy_import_stats',
2223
-
2224
- # Lazy mode facade functions
2225
- 'enable_lazy_mode',
2226
- 'disable_lazy_mode',
2227
- 'is_lazy_mode_enabled',
2228
- 'get_lazy_mode_stats',
2229
- 'configure_lazy_mode',
2230
- 'preload_modules',
2231
- 'optimize_lazy_mode',
2232
-
2233
- # Configuration
2234
- 'config_package_lazy_install_enabled',
2235
-
2236
- # Security & Policy
2237
- 'set_package_allow_list',
2238
- 'set_package_deny_list',
2239
- 'add_to_package_allow_list',
2240
- 'add_to_package_deny_list',
2241
- 'set_package_index_url',
2242
- 'set_package_extra_index_urls',
2243
- 'add_package_trusted_host',
2244
- 'set_package_lockfile',
2245
- 'generate_package_sbom',
2246
- 'check_externally_managed_environment',
2247
- ]
2248
-