exonware-xwlazy 0.1.0.21__py3-none-any.whl → 0.1.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/__init__.py +22 -6
- exonware/xwlazy/__init__.py +14 -2
- exonware/xwlazy/common/__init__.py +8 -0
- exonware/xwlazy/common/base.py +11 -2
- exonware/xwlazy/common/cache.py +5 -5
- exonware/xwlazy/common/logger.py +5 -5
- exonware/xwlazy/common/services/dependency_mapper.py +31 -13
- exonware/xwlazy/common/services/install_async_utils.py +5 -0
- exonware/xwlazy/common/services/install_cache_utils.py +4 -4
- exonware/xwlazy/common/services/spec_cache.py +2 -2
- exonware/xwlazy/common/services/state_manager.py +4 -4
- exonware/xwlazy/common/strategies/caching_dict.py +2 -2
- exonware/xwlazy/common/strategies/caching_lfu.py +2 -2
- exonware/xwlazy/common/strategies/caching_ttl.py +2 -2
- exonware/xwlazy/common/utils.py +142 -0
- exonware/xwlazy/config.py +1 -1
- exonware/xwlazy/contracts.py +162 -25
- exonware/xwlazy/defs.py +15 -15
- exonware/xwlazy/facade.py +175 -29
- exonware/xwlazy/host/__init__.py +8 -0
- exonware/xwlazy/host/conf.py +16 -0
- exonware/xwlazy/module/base.py +61 -4
- exonware/xwlazy/module/facade.py +1 -1
- exonware/xwlazy/module/importer_engine.py +1017 -170
- exonware/xwlazy/module/partial_module_detector.py +275 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +3 -3
- exonware/xwlazy/package/base.py +106 -41
- exonware/xwlazy/package/conf.py +6 -6
- exonware/xwlazy/package/services/config_manager.py +20 -16
- exonware/xwlazy/package/services/discovery.py +81 -16
- exonware/xwlazy/package/services/host_packages.py +41 -6
- exonware/xwlazy/package/services/install_async.py +16 -2
- exonware/xwlazy/package/services/install_cache.py +4 -4
- exonware/xwlazy/package/services/install_policy.py +14 -14
- exonware/xwlazy/package/services/install_registry.py +3 -3
- exonware/xwlazy/package/services/install_sbom.py +1 -1
- exonware/xwlazy/package/services/installer_engine.py +3 -3
- exonware/xwlazy/package/services/lazy_installer.py +102 -17
- exonware/xwlazy/package/services/manifest.py +43 -36
- exonware/xwlazy/package/services/strategy_registry.py +150 -12
- exonware/xwlazy/package/strategies/package_discovery_file.py +2 -2
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +2 -2
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_async.py +3 -3
- exonware/xwlazy/package/strategies/package_execution_cached.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_pip.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_wheel.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +2 -2
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +4 -4
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +4 -4
- exonware/xwlazy/package/strategies/package_policy_permissive.py +3 -3
- exonware/xwlazy/package/strategies/package_timing_clean.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_full.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_smart.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_temporary.py +2 -2
- exonware/xwlazy/runtime/adaptive_learner.py +7 -7
- exonware/xwlazy/runtime/base.py +14 -14
- exonware/xwlazy/runtime/facade.py +7 -7
- exonware/xwlazy/runtime/intelligent_selector.py +6 -6
- exonware/xwlazy/runtime/metrics.py +6 -6
- exonware/xwlazy/runtime/performance.py +5 -5
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.21.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/METADATA +2 -6
- exonware_xwlazy-0.1.0.23.dist-info/RECORD +93 -0
- xwlazy/__init__.py +14 -0
- xwlazy/lazy.py +30 -0
- exonware_xwlazy-0.1.0.21.dist-info/RECORD +0 -87
- {exonware_xwlazy-0.1.0.21.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.21.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -63,7 +63,7 @@ import subprocess
|
|
|
63
63
|
import concurrent.futures
|
|
64
64
|
from pathlib import Path
|
|
65
65
|
from types import ModuleType
|
|
66
|
-
from typing import
|
|
66
|
+
from typing import Optional, Any, Iterable, Callable
|
|
67
67
|
from collections import OrderedDict, defaultdict, Counter, deque
|
|
68
68
|
from queue import Queue
|
|
69
69
|
from datetime import datetime
|
|
@@ -103,12 +103,23 @@ _installing = threading.local()
|
|
|
103
103
|
_installation_depth = 0
|
|
104
104
|
_installation_depth_lock = threading.Lock()
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
# Thread-local flag to prevent recursion during installation checks
|
|
107
|
+
_checking_installation = threading.local()
|
|
108
|
+
|
|
109
|
+
def _get_thread_imports() -> set[str]:
|
|
107
110
|
"""Get thread-local import set (creates if needed)."""
|
|
108
111
|
if not hasattr(_thread_local, 'imports'):
|
|
109
112
|
_thread_local.imports = set()
|
|
110
113
|
return _thread_local.imports
|
|
111
114
|
|
|
115
|
+
def _is_checking_installation() -> bool:
|
|
116
|
+
"""Check if we're currently checking installation status (to prevent recursion)."""
|
|
117
|
+
return getattr(_checking_installation, 'active', False)
|
|
118
|
+
|
|
119
|
+
def _set_checking_installation(value: bool) -> None:
|
|
120
|
+
"""Set the installation check flag."""
|
|
121
|
+
_checking_installation.active = value
|
|
122
|
+
|
|
112
123
|
def _is_import_in_progress(module_name: str) -> bool:
|
|
113
124
|
"""Check if a module import is currently in progress for this thread."""
|
|
114
125
|
return module_name in _get_thread_imports()
|
|
@@ -137,6 +148,556 @@ _importing_state = get_importing_state()
|
|
|
137
148
|
_installation_depth = 0
|
|
138
149
|
_installation_depth_lock = threading.Lock()
|
|
139
150
|
|
|
151
|
+
# =============================================================================
|
|
152
|
+
# GLOBAL __import__ HOOK (Critical for Module-Level Imports)
|
|
153
|
+
# =============================================================================
|
|
154
|
+
|
|
155
|
+
# Global state for builtins.__import__ hook
|
|
156
|
+
_original_builtins_import: Optional[Callable] = None
|
|
157
|
+
_global_import_hook_installed: bool = False
|
|
158
|
+
_global_import_hook_lock = threading.RLock()
|
|
159
|
+
|
|
160
|
+
# Fast-path caches for O(1) lookup
|
|
161
|
+
_installed_cache: set[str] = set()
|
|
162
|
+
_failed_cache: set[str] = set()
|
|
163
|
+
|
|
164
|
+
# Registry of packages that should auto-install
|
|
165
|
+
_lazy_packages: dict[str, Any] = {}
|
|
166
|
+
|
|
167
|
+
def register_lazy_package(package_name: str, config: Optional[Any] = None) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Register a package for lazy loading/installation.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
package_name: Package name to register
|
|
173
|
+
config: Optional configuration object
|
|
174
|
+
"""
|
|
175
|
+
with _global_import_hook_lock:
|
|
176
|
+
_lazy_packages[package_name] = config or {}
|
|
177
|
+
logger.debug(f"Registered lazy package: {package_name}")
|
|
178
|
+
|
|
179
|
+
def _should_auto_install(module_name: str) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
Check if module should be auto-installed.
|
|
182
|
+
|
|
183
|
+
Checks if:
|
|
184
|
+
1. Module's root package is registered, OR
|
|
185
|
+
2. Module maps to a known package (via DependencyMapper) AND packages are registered
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
module_name: Module name to check
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if should auto-install, False otherwise
|
|
192
|
+
"""
|
|
193
|
+
root_package = module_name.split('.')[0]
|
|
194
|
+
|
|
195
|
+
# Fast path: root package is registered
|
|
196
|
+
if root_package in _lazy_packages:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
# If no packages are registered, don't auto-install
|
|
200
|
+
if not _lazy_packages:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
# Check if module maps to a known package via DependencyMapper
|
|
204
|
+
# This handles cases like:
|
|
205
|
+
# - 'yaml' -> 'PyYAML' (different name)
|
|
206
|
+
# - 'msgpack' -> 'msgpack' (same name, but still a known dependency)
|
|
207
|
+
# - 'bson' -> 'pymongo' (different name)
|
|
208
|
+
try:
|
|
209
|
+
# Use the first registered package to get the mapper context
|
|
210
|
+
# All registered packages should have the same dependency mappings
|
|
211
|
+
registered_package = next(iter(_lazy_packages.keys()), None)
|
|
212
|
+
if registered_package:
|
|
213
|
+
mapper = DependencyMapper(package_name=registered_package)
|
|
214
|
+
else:
|
|
215
|
+
mapper = DependencyMapper()
|
|
216
|
+
|
|
217
|
+
package_name = mapper.get_package_name(module_name)
|
|
218
|
+
|
|
219
|
+
# If DependencyMapper found a package name (not None), it's a known dependency
|
|
220
|
+
# Allow installation attempt - the installer will verify if it's actually needed
|
|
221
|
+
if package_name:
|
|
222
|
+
logger.debug(f"[AUTO-INSTALL] Module '{module_name}' maps to package '{package_name}', allowing auto-install")
|
|
223
|
+
return True
|
|
224
|
+
else:
|
|
225
|
+
logger.debug(f"[AUTO-INSTALL] Module '{module_name}' has no package mapping, skipping auto-install")
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
# If mapper fails, log and be conservative
|
|
229
|
+
logger.debug(f"[AUTO-INSTALL] DependencyMapper failed for '{module_name}': {e}")
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def _try_install_package(module_name: str) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
Try to install package for missing module.
|
|
237
|
+
|
|
238
|
+
Tries all registered packages until one succeeds.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
module_name: Module name that failed to import
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if installation successful, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
# Try root package first
|
|
247
|
+
root_package = module_name.split('.')[0]
|
|
248
|
+
if root_package in _lazy_packages:
|
|
249
|
+
try:
|
|
250
|
+
logger.info(f"[AUTO-INSTALL] Trying root package {root_package} for module {module_name}")
|
|
251
|
+
installer = LazyInstallerRegistry.get_instance(root_package)
|
|
252
|
+
logger.info(f"[AUTO-INSTALL] Installer for {root_package}: {installer is not None}, enabled={installer.is_enabled() if installer else False}")
|
|
253
|
+
|
|
254
|
+
if installer is None:
|
|
255
|
+
logger.warning(f"[AUTO-INSTALL] Installer for {root_package} is None")
|
|
256
|
+
elif not installer.is_enabled():
|
|
257
|
+
logger.warning(f"[AUTO-INSTALL] Installer for {root_package} is disabled")
|
|
258
|
+
else:
|
|
259
|
+
logger.info(f"Auto-installing missing package for module: {module_name} (via {root_package})")
|
|
260
|
+
logger.info(f"[AUTO-INSTALL] Calling install_and_import('{module_name}')")
|
|
261
|
+
try:
|
|
262
|
+
module, success = installer.install_and_import(module_name)
|
|
263
|
+
logger.info(f"[AUTO-INSTALL] install_and_import returned: module={module is not None}, success={success}")
|
|
264
|
+
if success and module:
|
|
265
|
+
_installed_cache.add(module_name)
|
|
266
|
+
logger.info(f"[AUTO-INSTALL] Successfully installed and imported '{module_name}'")
|
|
267
|
+
return True
|
|
268
|
+
else:
|
|
269
|
+
logger.warning(f"[AUTO-INSTALL] install_and_import failed for '{module_name}': success={success}, module={module is not None}")
|
|
270
|
+
except Exception as install_exc:
|
|
271
|
+
logger.error(f"[AUTO-INSTALL] install_and_import raised exception: {install_exc}", exc_info=True)
|
|
272
|
+
raise
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"[AUTO-INSTALL] Exception installing via root package {root_package}: {e}", exc_info=True)
|
|
275
|
+
|
|
276
|
+
# Try all registered packages (for dependencies like 'yaml' when 'xwsystem' is registered)
|
|
277
|
+
for registered_package in _lazy_packages.keys():
|
|
278
|
+
if registered_package == root_package:
|
|
279
|
+
continue # Already tried
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
logger.debug(f"[AUTO-INSTALL] Trying to get installer for {registered_package}")
|
|
283
|
+
installer = LazyInstallerRegistry.get_instance(registered_package)
|
|
284
|
+
logger.debug(f"[AUTO-INSTALL] Installer retrieved: {installer is not None}")
|
|
285
|
+
|
|
286
|
+
if installer is None:
|
|
287
|
+
logger.warning(f"[AUTO-INSTALL] Installer for {registered_package} is None")
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
if not installer.is_enabled():
|
|
291
|
+
logger.warning(f"[AUTO-INSTALL] Installer for {registered_package} is disabled")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
logger.info(f"Auto-installing missing package for module: {module_name} (via {registered_package})")
|
|
295
|
+
logger.info(f"[AUTO-INSTALL] Calling install_and_import('{module_name}')")
|
|
296
|
+
try:
|
|
297
|
+
module, success = installer.install_and_import(module_name)
|
|
298
|
+
logger.info(f"[AUTO-INSTALL] install_and_import returned: module={module is not None}, success={success}")
|
|
299
|
+
except Exception as install_exc:
|
|
300
|
+
logger.error(f"[AUTO-INSTALL] install_and_import raised exception: {install_exc}", exc_info=True)
|
|
301
|
+
raise
|
|
302
|
+
|
|
303
|
+
if success and module:
|
|
304
|
+
_installed_cache.add(module_name)
|
|
305
|
+
logger.info(f"[AUTO-INSTALL] Successfully installed and imported '{module_name}'")
|
|
306
|
+
return True
|
|
307
|
+
else:
|
|
308
|
+
logger.warning(f"[AUTO-INSTALL] install_and_import failed for '{module_name}': success={success}, module={module is not None}")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"[AUTO-INSTALL] Exception installing via {registered_package}: {e}", exc_info=True)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
logger.warning(f"[AUTO-INSTALL] Failed to install package for module '{module_name}' via all registered packages")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def _intercepting_import(name: str, globals=None, locals=None, fromlist=(), level=0):
|
|
317
|
+
"""
|
|
318
|
+
Intercept ALL imports including module-level ones.
|
|
319
|
+
|
|
320
|
+
This is the global builtins.__import__ replacement that catches
|
|
321
|
+
ALL imports, including those at module level during package initialization.
|
|
322
|
+
|
|
323
|
+
CRITICAL: Skip relative imports (level > 0) - they must use normal import path.
|
|
324
|
+
Relative imports are package/module agnostic and should not be intercepted.
|
|
325
|
+
|
|
326
|
+
CRITICAL: When fromlist is non-empty (e.g., "from module import Class"),
|
|
327
|
+
Python's import machinery needs direct access to module attributes via getattr().
|
|
328
|
+
We MUST return the actual module object without any wrapping.
|
|
329
|
+
"""
|
|
330
|
+
# CRITICAL FIX: Handle relative imports (level > 0)
|
|
331
|
+
# Relative imports like "from .common import" have level=1
|
|
332
|
+
# We must use normal import BUT still enhance the module after import (package/module agnostic)
|
|
333
|
+
if level > 0:
|
|
334
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
335
|
+
# CRITICAL: Enhance modules imported via relative imports (package/module agnostic)
|
|
336
|
+
# This ensures instance methods work on classes for ANY package/module structure
|
|
337
|
+
if result and isinstance(result, ModuleType) and fromlist:
|
|
338
|
+
try:
|
|
339
|
+
for pkg_name in _lazy_packages.keys():
|
|
340
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
341
|
+
finder = _installed_hooks.get(pkg_name)
|
|
342
|
+
if finder:
|
|
343
|
+
finder._enhance_classes_with_class_methods(result)
|
|
344
|
+
break
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
# CRITICAL FIX: Handle fromlist imports specially
|
|
350
|
+
# When fromlist is present (e.g., "from module import Class"),
|
|
351
|
+
# Python needs direct access to module attributes via getattr(module, 'Class')
|
|
352
|
+
# We MUST return the actual module without any wrapping or modification
|
|
353
|
+
if fromlist:
|
|
354
|
+
# Import partial module detector (lazy import to avoid circular dependency)
|
|
355
|
+
try:
|
|
356
|
+
from .partial_module_detector import (
|
|
357
|
+
is_partially_initialized as _default_is_partially_initialized,
|
|
358
|
+
mark_module_importing,
|
|
359
|
+
unmark_module_importing,
|
|
360
|
+
DetectionStrategy,
|
|
361
|
+
PartialModuleDetector
|
|
362
|
+
)
|
|
363
|
+
# Allow strategy override via environment variable for testing
|
|
364
|
+
strategy_name = os.environ.get('XWLAZY_PARTIAL_DETECTION_STRATEGY', 'hybrid')
|
|
365
|
+
try:
|
|
366
|
+
strategy = DetectionStrategy(strategy_name)
|
|
367
|
+
detector = PartialModuleDetector(strategy)
|
|
368
|
+
is_partially_initialized = lambda n, m: detector.is_partially_initialized(n, m)
|
|
369
|
+
except (ValueError, AttributeError):
|
|
370
|
+
# Use default detector (HYBRID strategy)
|
|
371
|
+
is_partially_initialized = _default_is_partially_initialized
|
|
372
|
+
except ImportError:
|
|
373
|
+
# Fallback if detector not available
|
|
374
|
+
def is_partially_initialized(name, mod):
|
|
375
|
+
return False
|
|
376
|
+
def mark_module_importing(name):
|
|
377
|
+
pass
|
|
378
|
+
def unmark_module_importing(name):
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
# Mark this module as being imported (for tracking)
|
|
382
|
+
mark_module_importing(name)
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
# Fast path: already imported and in sys.modules
|
|
386
|
+
# CRITICAL: For fromlist imports, we must be VERY conservative
|
|
387
|
+
# Only return early if we're CERTAIN the module is fully initialized
|
|
388
|
+
# and NOT currently being imported
|
|
389
|
+
if name in sys.modules:
|
|
390
|
+
module = sys.modules[name]
|
|
391
|
+
# Ensure module is fully loaded (not a placeholder or lazy wrapper)
|
|
392
|
+
# Check if it's a real ModuleType (not a proxy/wrapper)
|
|
393
|
+
if isinstance(module, ModuleType):
|
|
394
|
+
# CRITICAL: Check if module is partially initialized
|
|
395
|
+
# We must NOT return a partially initialized module
|
|
396
|
+
# For fromlist imports, be extra conservative - only return if
|
|
397
|
+
# module is definitely fully loaded AND not currently importing
|
|
398
|
+
if not is_partially_initialized(name, module):
|
|
399
|
+
# Additional check: verify module has meaningful content
|
|
400
|
+
# (not just metadata attributes)
|
|
401
|
+
module_dict = getattr(module, '__dict__', {})
|
|
402
|
+
metadata_attrs = {'__name__', '__loader__', '__spec__', '__package__', '__file__', '__path__', '__cached__'}
|
|
403
|
+
content_attrs = set(module_dict.keys()) - metadata_attrs
|
|
404
|
+
|
|
405
|
+
# Only return if module has actual content (classes, functions, etc.)
|
|
406
|
+
# OR if it's a namespace package (has __path__)
|
|
407
|
+
has_content = len(content_attrs) > 0
|
|
408
|
+
is_namespace = hasattr(module, '__path__')
|
|
409
|
+
|
|
410
|
+
if has_content or is_namespace:
|
|
411
|
+
# Check if it's a placeholder by looking for common placeholder patterns
|
|
412
|
+
is_placeholder = (
|
|
413
|
+
hasattr(module, '__getattr__') and
|
|
414
|
+
not hasattr(module, '__file__') and
|
|
415
|
+
not hasattr(module, '__path__') # Namespace packages don't have __file__
|
|
416
|
+
)
|
|
417
|
+
if not is_placeholder:
|
|
418
|
+
# Module is fully loaded with content, return it directly
|
|
419
|
+
# CRITICAL FIX: Enhance classes before returning
|
|
420
|
+
try:
|
|
421
|
+
# Find which package this module belongs to
|
|
422
|
+
for pkg_name in _lazy_packages.keys():
|
|
423
|
+
# Check if module name starts with package name or "exonware." + package name
|
|
424
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
425
|
+
finder = _installed_hooks.get(pkg_name)
|
|
426
|
+
if finder:
|
|
427
|
+
finder._enhance_classes_with_class_methods(module)
|
|
428
|
+
break
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
unmark_module_importing(name)
|
|
432
|
+
return module
|
|
433
|
+
# Module is partially initialized or has no content - fall through to normal import
|
|
434
|
+
# If it's a placeholder, partially initialized, or has no content, fall through to normal import
|
|
435
|
+
finally:
|
|
436
|
+
# Always unmark when done (even if exception occurs)
|
|
437
|
+
unmark_module_importing(name)
|
|
438
|
+
|
|
439
|
+
# For fromlist imports, use normal import path to ensure classes/functions
|
|
440
|
+
# are accessible via getattr() - Python's import machinery handles extraction
|
|
441
|
+
try:
|
|
442
|
+
# Mark as importing before calling original import
|
|
443
|
+
try:
|
|
444
|
+
from .partial_module_detector import mark_module_importing, unmark_module_importing
|
|
445
|
+
mark_module_importing(name)
|
|
446
|
+
except ImportError:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
451
|
+
# Cache success but return actual module (no wrapping)
|
|
452
|
+
_installed_cache.add(name)
|
|
453
|
+
# CRITICAL FIX: Enhance classes in the module for class-level method access
|
|
454
|
+
# This makes instance methods callable on classes (e.g., BsonSerializer.encode(data))
|
|
455
|
+
if result and isinstance(result, ModuleType):
|
|
456
|
+
try:
|
|
457
|
+
# Find which package this module belongs to
|
|
458
|
+
for pkg_name in _lazy_packages.keys():
|
|
459
|
+
# Check if module name starts with package name or "exonware." + package name
|
|
460
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
461
|
+
finder = _installed_hooks.get(pkg_name)
|
|
462
|
+
if finder:
|
|
463
|
+
finder._enhance_classes_with_class_methods(result)
|
|
464
|
+
break
|
|
465
|
+
except Exception as e:
|
|
466
|
+
# Enhancement failed - don't break the import, but log for debugging
|
|
467
|
+
logger.debug(f"Enhancement failed for {name}: {e}", exc_info=True)
|
|
468
|
+
pass
|
|
469
|
+
return result
|
|
470
|
+
finally:
|
|
471
|
+
# Always unmark when done
|
|
472
|
+
try:
|
|
473
|
+
from .partial_module_detector import unmark_module_importing
|
|
474
|
+
unmark_module_importing(name)
|
|
475
|
+
except ImportError:
|
|
476
|
+
pass
|
|
477
|
+
except ImportError as e:
|
|
478
|
+
# Only try auto-install if needed
|
|
479
|
+
if _should_auto_install(name):
|
|
480
|
+
try:
|
|
481
|
+
if _try_install_package(name):
|
|
482
|
+
# Retry import after installation - return actual module
|
|
483
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
484
|
+
_installed_cache.add(name)
|
|
485
|
+
# CRITICAL FIX: Enhance classes in the module for class-level method access
|
|
486
|
+
if result and isinstance(result, ModuleType):
|
|
487
|
+
try:
|
|
488
|
+
# Find which package this module belongs to
|
|
489
|
+
for pkg_name in _lazy_packages.keys():
|
|
490
|
+
# Check if module name starts with package name or "exonware." + package name
|
|
491
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
492
|
+
finder = _installed_hooks.get(pkg_name)
|
|
493
|
+
if finder:
|
|
494
|
+
finder._enhance_classes_with_class_methods(result)
|
|
495
|
+
break
|
|
496
|
+
except Exception:
|
|
497
|
+
pass
|
|
498
|
+
return result
|
|
499
|
+
except Exception:
|
|
500
|
+
# If installation fails, don't crash - just raise the original ImportError
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
# Installation failed or not applicable - cache failure (but limit cache size)
|
|
504
|
+
if len(_failed_cache) < 1000: # Prevent unbounded growth
|
|
505
|
+
_failed_cache.add(name)
|
|
506
|
+
raise
|
|
507
|
+
|
|
508
|
+
# Fast path: cached as installed (but still enhance for fromlist)
|
|
509
|
+
if name in _installed_cache:
|
|
510
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
511
|
+
# CRITICAL: Enhance even cached modules for fromlist imports (package/module agnostic)
|
|
512
|
+
if fromlist and result and isinstance(result, ModuleType):
|
|
513
|
+
try:
|
|
514
|
+
for pkg_name in _lazy_packages.keys():
|
|
515
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
516
|
+
finder = _installed_hooks.get(pkg_name)
|
|
517
|
+
if finder:
|
|
518
|
+
finder._enhance_classes_with_class_methods(result)
|
|
519
|
+
break
|
|
520
|
+
except Exception:
|
|
521
|
+
pass
|
|
522
|
+
return result
|
|
523
|
+
|
|
524
|
+
# Fast path: already imported (for non-fromlist imports)
|
|
525
|
+
# CRITICAL: For fromlist imports, enhance even if module is cached
|
|
526
|
+
if name in sys.modules:
|
|
527
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
528
|
+
if fromlist:
|
|
529
|
+
module = sys.modules.get(name)
|
|
530
|
+
if module and isinstance(module, ModuleType):
|
|
531
|
+
try:
|
|
532
|
+
for pkg_name in _lazy_packages.keys():
|
|
533
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
534
|
+
finder = _installed_hooks.get(pkg_name)
|
|
535
|
+
if finder:
|
|
536
|
+
finder._enhance_classes_with_class_methods(module)
|
|
537
|
+
break
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
return result
|
|
541
|
+
|
|
542
|
+
# Fast path: known failure
|
|
543
|
+
if name in _failed_cache:
|
|
544
|
+
raise ImportError(f"No module named '{name}'")
|
|
545
|
+
|
|
546
|
+
# Fast path: skip stdlib/builtin modules (performance optimization)
|
|
547
|
+
if name in sys.builtin_module_names:
|
|
548
|
+
return _original_builtins_import(name, globals, locals, fromlist, level)
|
|
549
|
+
|
|
550
|
+
# Skip private/internal modules (performance optimization)
|
|
551
|
+
# But allow if it's a submodule of a registered package
|
|
552
|
+
if name.startswith('_'):
|
|
553
|
+
# Check if it's a submodule of a registered package
|
|
554
|
+
root_package = name.split('.')[0]
|
|
555
|
+
if root_package not in _lazy_packages:
|
|
556
|
+
return _original_builtins_import(name, globals, locals, fromlist, level)
|
|
557
|
+
|
|
558
|
+
# Skip test-related modules to avoid interfering with pytest
|
|
559
|
+
if name.startswith(('pytest', '_pytest', 'pluggy', '_pluggy')):
|
|
560
|
+
return _original_builtins_import(name, globals, locals, fromlist, level)
|
|
561
|
+
|
|
562
|
+
# Skip debugging/profiling modules
|
|
563
|
+
if name in ('tracemalloc', 'pdb', 'ipdb', 'debugpy', 'pydevd'):
|
|
564
|
+
return _original_builtins_import(name, globals, locals, fromlist, level)
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Try normal import first
|
|
568
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
569
|
+
# Success - cache it
|
|
570
|
+
_installed_cache.add(name)
|
|
571
|
+
# CRITICAL FIX: Enhance classes for fromlist imports (package/module agnostic)
|
|
572
|
+
# This ensures instance methods work on classes for ANY package/module structure
|
|
573
|
+
if result and isinstance(result, ModuleType) and fromlist:
|
|
574
|
+
try:
|
|
575
|
+
for pkg_name in _lazy_packages.keys():
|
|
576
|
+
if name.startswith(pkg_name) or name.startswith(f"exonware.{pkg_name}"):
|
|
577
|
+
finder = _installed_hooks.get(pkg_name)
|
|
578
|
+
if finder:
|
|
579
|
+
finder._enhance_classes_with_class_methods(result)
|
|
580
|
+
break
|
|
581
|
+
except Exception:
|
|
582
|
+
pass
|
|
583
|
+
return result
|
|
584
|
+
except ImportError as e:
|
|
585
|
+
# Check if this package should be auto-installed
|
|
586
|
+
# ROOT CAUSE DEBUG: Log the exact import name and traceback to find where typos originate
|
|
587
|
+
if any(typo in name for typo in ['contrrib', 'msgpackk', 'msgppack', 'mmsgpack']):
|
|
588
|
+
import traceback
|
|
589
|
+
logger.warning(
|
|
590
|
+
f"[ROOT CAUSE] Typo detected in module name '{name}'. "
|
|
591
|
+
f"ImportError: {e}. "
|
|
592
|
+
f"Traceback:\n{''.join(traceback.format_stack()[-5:-1])}"
|
|
593
|
+
)
|
|
594
|
+
logger.debug(f"[AUTO-INSTALL] ImportError for '{name}': {e}")
|
|
595
|
+
should_install = _should_auto_install(name)
|
|
596
|
+
logger.debug(f"[AUTO-INSTALL] Should auto-install '{name}': {should_install}")
|
|
597
|
+
|
|
598
|
+
if should_install:
|
|
599
|
+
try:
|
|
600
|
+
logger.info(f"[AUTO-INSTALL] Attempting to install package for '{name}'")
|
|
601
|
+
if _try_install_package(name):
|
|
602
|
+
logger.info(f"[AUTO-INSTALL] Successfully installed package for '{name}', retrying import")
|
|
603
|
+
# Retry import after installation
|
|
604
|
+
try:
|
|
605
|
+
result = _original_builtins_import(name, globals, locals, fromlist, level)
|
|
606
|
+
_installed_cache.add(name)
|
|
607
|
+
logger.info(f"[AUTO-INSTALL] Successfully imported '{name}' after installation")
|
|
608
|
+
return result
|
|
609
|
+
except ImportError as retry_error:
|
|
610
|
+
logger.warning(f"[AUTO-INSTALL] Import still failed for '{name}' after installation: {retry_error}")
|
|
611
|
+
pass
|
|
612
|
+
else:
|
|
613
|
+
logger.warning(f"[AUTO-INSTALL] Installation attempt returned False for '{name}'")
|
|
614
|
+
except Exception as install_error:
|
|
615
|
+
# If installation fails, log it but don't crash - just raise the original ImportError
|
|
616
|
+
logger.error(f"[AUTO-INSTALL] Installation failed for '{name}': {install_error}", exc_info=True)
|
|
617
|
+
pass
|
|
618
|
+
else:
|
|
619
|
+
logger.debug(f"[AUTO-INSTALL] Not auto-installing '{name}' (not eligible)")
|
|
620
|
+
|
|
621
|
+
# Installation failed or not applicable - cache failure (but limit cache size)
|
|
622
|
+
if len(_failed_cache) < 1000: # Prevent unbounded growth
|
|
623
|
+
_failed_cache.add(name)
|
|
624
|
+
raise
|
|
625
|
+
except Exception:
|
|
626
|
+
# For any other exception, don't interfere - let it propagate
|
|
627
|
+
# This prevents the hook from breaking system functionality
|
|
628
|
+
raise
|
|
629
|
+
|
|
630
|
+
def install_global_import_hook() -> None:
|
|
631
|
+
"""
|
|
632
|
+
Install global builtins.__import__ hook for auto-install.
|
|
633
|
+
|
|
634
|
+
This hook intercepts ALL imports including module-level ones,
|
|
635
|
+
enabling auto-installation for registered packages.
|
|
636
|
+
"""
|
|
637
|
+
global _original_builtins_import, _global_import_hook_installed
|
|
638
|
+
|
|
639
|
+
with _global_import_hook_lock:
|
|
640
|
+
if _global_import_hook_installed:
|
|
641
|
+
logger.debug("Global import hook already installed")
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
if _original_builtins_import is None:
|
|
645
|
+
_original_builtins_import = builtins.__import__
|
|
646
|
+
|
|
647
|
+
builtins.__import__ = _intercepting_import
|
|
648
|
+
_global_import_hook_installed = True
|
|
649
|
+
logger.info("✅ Global builtins.__import__ hook installed for auto-install")
|
|
650
|
+
|
|
651
|
+
def uninstall_global_import_hook() -> None:
|
|
652
|
+
"""
|
|
653
|
+
Uninstall global builtins.__import__ hook.
|
|
654
|
+
|
|
655
|
+
Restores original builtins.__import__.
|
|
656
|
+
"""
|
|
657
|
+
global _original_builtins_import, _global_import_hook_installed
|
|
658
|
+
|
|
659
|
+
with _global_import_hook_lock:
|
|
660
|
+
if not _global_import_hook_installed:
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
if _original_builtins_import is not None:
|
|
664
|
+
builtins.__import__ = _original_builtins_import
|
|
665
|
+
_original_builtins_import = None
|
|
666
|
+
|
|
667
|
+
_global_import_hook_installed = False
|
|
668
|
+
logger.info("Global builtins.__import__ hook uninstalled")
|
|
669
|
+
|
|
670
|
+
def is_global_import_hook_installed() -> bool:
|
|
671
|
+
"""Check if global import hook is installed."""
|
|
672
|
+
return _global_import_hook_installed
|
|
673
|
+
|
|
674
|
+
def clear_global_import_caches() -> None:
|
|
675
|
+
"""
|
|
676
|
+
Clear global import hook caches (useful for testing).
|
|
677
|
+
|
|
678
|
+
Clears both installed and failed caches.
|
|
679
|
+
"""
|
|
680
|
+
global _installed_cache, _failed_cache
|
|
681
|
+
with _global_import_hook_lock:
|
|
682
|
+
_installed_cache.clear()
|
|
683
|
+
_failed_cache.clear()
|
|
684
|
+
logger.debug("Cleared global import hook caches")
|
|
685
|
+
|
|
686
|
+
def get_global_import_cache_stats() -> dict[str, Any]:
|
|
687
|
+
"""
|
|
688
|
+
Get statistics about global import hook caches.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Dict with cache sizes and hit/miss information
|
|
692
|
+
"""
|
|
693
|
+
with _global_import_hook_lock:
|
|
694
|
+
return {
|
|
695
|
+
'installed_cache_size': len(_installed_cache),
|
|
696
|
+
'failed_cache_size': len(_failed_cache),
|
|
697
|
+
'registered_packages': list(_lazy_packages.keys()),
|
|
698
|
+
'hook_installed': _global_import_hook_installed,
|
|
699
|
+
}
|
|
700
|
+
|
|
140
701
|
# =============================================================================
|
|
141
702
|
# PREFIX TRIE (from prefix_trie.py)
|
|
142
703
|
# =============================================================================
|
|
@@ -147,7 +708,7 @@ class _PrefixTrie:
|
|
|
147
708
|
__slots__ = ("_root",)
|
|
148
709
|
|
|
149
710
|
def __init__(self) -> None:
|
|
150
|
-
self._root:
|
|
711
|
+
self._root: dict[str, dict[str, Any]] = {}
|
|
151
712
|
|
|
152
713
|
def add(self, prefix: str) -> None:
|
|
153
714
|
"""Add a prefix to the trie."""
|
|
@@ -156,10 +717,10 @@ class _PrefixTrie:
|
|
|
156
717
|
node = node.setdefault(char, {})
|
|
157
718
|
node["_end"] = prefix
|
|
158
719
|
|
|
159
|
-
def iter_matches(self, value: str) ->
|
|
720
|
+
def iter_matches(self, value: str) -> tuple[str, ...]:
|
|
160
721
|
"""Find all matching prefixes for a given value."""
|
|
161
722
|
node = self._root
|
|
162
|
-
matches:
|
|
723
|
+
matches: list[str] = []
|
|
163
724
|
for char in value:
|
|
164
725
|
end_value = node.get("_end")
|
|
165
726
|
if end_value:
|
|
@@ -192,15 +753,15 @@ class WatchedPrefixRegistry:
|
|
|
192
753
|
"_root_snapshot_dirty",
|
|
193
754
|
)
|
|
194
755
|
|
|
195
|
-
def __init__(self, initial: Optional[
|
|
756
|
+
def __init__(self, initial: Optional[list[str]] = None) -> None:
|
|
196
757
|
self._lock = threading.RLock()
|
|
197
758
|
self._prefix_refcounts: Counter[str] = Counter()
|
|
198
|
-
self._owner_map:
|
|
199
|
-
self._prefixes:
|
|
759
|
+
self._owner_map: dict[str, set[str]] = {}
|
|
760
|
+
self._prefixes: set[str] = set()
|
|
200
761
|
self._trie = _PrefixTrie()
|
|
201
762
|
self._dirty = False
|
|
202
763
|
self._root_refcounts: Counter[str] = Counter()
|
|
203
|
-
self._root_snapshot:
|
|
764
|
+
self._root_snapshot: set[str] = set()
|
|
204
765
|
self._root_snapshot_dirty = False
|
|
205
766
|
if initial:
|
|
206
767
|
for prefix in initial:
|
|
@@ -287,7 +848,7 @@ class WatchedPrefixRegistry:
|
|
|
287
848
|
return True
|
|
288
849
|
return normalized in self._owner_map.get(owner_key, set())
|
|
289
850
|
|
|
290
|
-
def get_matching_prefixes(self, module_name: str) ->
|
|
851
|
+
def get_matching_prefixes(self, module_name: str) -> tuple[str, ...]:
|
|
291
852
|
with self._lock:
|
|
292
853
|
if not self._prefixes:
|
|
293
854
|
return ()
|
|
@@ -367,12 +928,12 @@ class ParallelLoader:
|
|
|
367
928
|
)
|
|
368
929
|
return self._executor
|
|
369
930
|
|
|
370
|
-
def load_modules_parallel(self, module_paths:
|
|
931
|
+
def load_modules_parallel(self, module_paths: list[str]) -> dict[str, Any]:
|
|
371
932
|
"""Load multiple modules in parallel."""
|
|
372
933
|
executor = self._get_executor()
|
|
373
|
-
results:
|
|
934
|
+
results: dict[str, Any] = {}
|
|
374
935
|
|
|
375
|
-
def _load_module(module_path: str) ->
|
|
936
|
+
def _load_module(module_path: str) -> tuple[str, Any, Optional[Exception]]:
|
|
376
937
|
try:
|
|
377
938
|
module = importlib.import_module(module_path)
|
|
378
939
|
return (module_path, module, None)
|
|
@@ -390,8 +951,8 @@ class ParallelLoader:
|
|
|
390
951
|
|
|
391
952
|
def load_modules_with_priority(
|
|
392
953
|
self,
|
|
393
|
-
module_paths:
|
|
394
|
-
) ->
|
|
954
|
+
module_paths: list[tuple[str, int]]
|
|
955
|
+
) -> dict[str, Any]:
|
|
395
956
|
"""Load modules in parallel with priority ordering."""
|
|
396
957
|
sorted_modules = sorted(module_paths, key=lambda x: x[1], reverse=True)
|
|
397
958
|
module_list = [path for path, _ in sorted_modules]
|
|
@@ -408,11 +969,11 @@ class DependencyGraph:
|
|
|
408
969
|
"""Manages module dependencies for optimal parallel loading."""
|
|
409
970
|
|
|
410
971
|
def __init__(self):
|
|
411
|
-
self._dependencies:
|
|
412
|
-
self._reverse_deps:
|
|
972
|
+
self._dependencies: dict[str, list[str]] = {}
|
|
973
|
+
self._reverse_deps: dict[str, list[str]] = {}
|
|
413
974
|
self._lock = threading.RLock()
|
|
414
975
|
|
|
415
|
-
def add_dependency(self, module: str, depends_on:
|
|
976
|
+
def add_dependency(self, module: str, depends_on: list[str]) -> None:
|
|
416
977
|
"""Add dependencies for a module."""
|
|
417
978
|
with self._lock:
|
|
418
979
|
self._dependencies[module] = depends_on
|
|
@@ -422,17 +983,17 @@ class DependencyGraph:
|
|
|
422
983
|
if module not in self._reverse_deps[dep]:
|
|
423
984
|
self._reverse_deps[dep].append(module)
|
|
424
985
|
|
|
425
|
-
def get_load_order(self, modules:
|
|
986
|
+
def get_load_order(self, modules: list[str]) -> list[list[str]]:
|
|
426
987
|
"""Get optimal load order for parallel loading (topological sort levels)."""
|
|
427
988
|
with self._lock:
|
|
428
|
-
in_degree:
|
|
989
|
+
in_degree: dict[str, int] = {m: 0 for m in modules}
|
|
429
990
|
for module, deps in self._dependencies.items():
|
|
430
991
|
if module in modules:
|
|
431
992
|
for dep in deps:
|
|
432
993
|
if dep in modules:
|
|
433
994
|
in_degree[module] += 1
|
|
434
995
|
|
|
435
|
-
levels:
|
|
996
|
+
levels: list[list[str]] = []
|
|
436
997
|
remaining = set(modules)
|
|
437
998
|
|
|
438
999
|
while remaining:
|
|
@@ -472,9 +1033,23 @@ def _lazy_aware_import_module(name: str, package: Optional[str] = None) -> Modul
|
|
|
472
1033
|
_mark_import_finished(name)
|
|
473
1034
|
|
|
474
1035
|
def _patch_import_module() -> None:
|
|
475
|
-
"""
|
|
476
|
-
importlib.import_module
|
|
477
|
-
|
|
1036
|
+
"""
|
|
1037
|
+
Patch importlib.import_module to be lazy-aware.
|
|
1038
|
+
|
|
1039
|
+
WARNING: This performs global monkey-patching that affects ALL code in the Python process.
|
|
1040
|
+
This can cause conflicts with other libraries. Use sys.meta_path hooks instead.
|
|
1041
|
+
|
|
1042
|
+
This function is kept for backward compatibility but should be avoided in new code.
|
|
1043
|
+
"""
|
|
1044
|
+
# DISABLED: Dangerous monkey-patching disabled by default
|
|
1045
|
+
logger.warning(
|
|
1046
|
+
"_patch_import_module called - Dangerous monkey-patching is DISABLED. "
|
|
1047
|
+
"Use sys.meta_path hooks (install_import_hook) instead."
|
|
1048
|
+
)
|
|
1049
|
+
return
|
|
1050
|
+
# Original code (disabled):
|
|
1051
|
+
# importlib.import_module = _lazy_aware_import_module
|
|
1052
|
+
# logger.debug("Patched importlib.import_module to be lazy-aware")
|
|
478
1053
|
|
|
479
1054
|
def _unpatch_import_module() -> None:
|
|
480
1055
|
"""Restore original importlib.import_module."""
|
|
@@ -533,7 +1108,7 @@ def bootstrap_lazy_mode(package_name: str) -> None:
|
|
|
533
1108
|
enabled = env_enabled
|
|
534
1109
|
|
|
535
1110
|
if enabled is None:
|
|
536
|
-
from
|
|
1111
|
+
from ..common.services.keyword_detection import _detect_lazy_installation
|
|
537
1112
|
enabled = _detect_lazy_installation(package_name)
|
|
538
1113
|
|
|
539
1114
|
if not enabled:
|
|
@@ -548,35 +1123,28 @@ def bootstrap_lazy_mode(package_name: str) -> None:
|
|
|
548
1123
|
)
|
|
549
1124
|
|
|
550
1125
|
def bootstrap_lazy_mode_deferred(package_name: str) -> None:
|
|
551
|
-
"""
|
|
552
|
-
|
|
553
|
-
package_module_name = f"exonware.{package_name_lower}"
|
|
1126
|
+
"""
|
|
1127
|
+
Schedule lazy mode bootstrap to run AFTER the calling package finishes importing.
|
|
554
1128
|
|
|
555
|
-
|
|
1129
|
+
WARNING: This function performs dangerous global monkey-patching of __builtins__.__import__.
|
|
1130
|
+
This can cause conflicts with other libraries (gevent, greenlet, debuggers, etc.).
|
|
1131
|
+
Consider using sys.meta_path hooks instead (install_import_hook) which is safer.
|
|
556
1132
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
else:
|
|
567
|
-
import builtins
|
|
568
|
-
builtins.__import__ = original_import
|
|
569
|
-
bootstrap_lazy_mode(package_name_lower)
|
|
570
|
-
|
|
571
|
-
threading.Timer(0.0, _install_hook).start()
|
|
572
|
-
|
|
573
|
-
return result
|
|
1133
|
+
This function is kept for backward compatibility but should be avoided in new code.
|
|
1134
|
+
"""
|
|
1135
|
+
# DISABLED: Dangerous monkey-patching disabled by default
|
|
1136
|
+
# Uncomment only if absolutely necessary and you understand the risks
|
|
1137
|
+
logger.warning(
|
|
1138
|
+
f"bootstrap_lazy_mode_deferred called for {package_name} - "
|
|
1139
|
+
"Dangerous monkey-patching is DISABLED. Use install_import_hook() instead."
|
|
1140
|
+
)
|
|
1141
|
+
return
|
|
574
1142
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1143
|
+
# Original code (disabled):
|
|
1144
|
+
# package_name_lower = package_name.lower()
|
|
1145
|
+
# package_module_name = f"exonware.{package_name_lower}"
|
|
1146
|
+
# original_import = __builtins__.__import__ if hasattr(__builtins__, '__import__') else __import__
|
|
1147
|
+
# ... (rest of dangerous code)
|
|
580
1148
|
|
|
581
1149
|
# =============================================================================
|
|
582
1150
|
# LAZY LOADER (from loader.py)
|
|
@@ -650,9 +1218,9 @@ class LazyModuleRegistry:
|
|
|
650
1218
|
__slots__ = ('_modules', '_load_times', '_lock', '_access_counts')
|
|
651
1219
|
|
|
652
1220
|
def __init__(self):
|
|
653
|
-
self._modules:
|
|
654
|
-
self._load_times:
|
|
655
|
-
self._access_counts:
|
|
1221
|
+
self._modules: dict[str, LazyLoader] = {}
|
|
1222
|
+
self._load_times: dict[str, float] = {}
|
|
1223
|
+
self._access_counts: dict[str, int] = {}
|
|
656
1224
|
self._lock = threading.RLock()
|
|
657
1225
|
|
|
658
1226
|
def register_module(self, name: str, module_path: str) -> None:
|
|
@@ -687,7 +1255,7 @@ class LazyModuleRegistry:
|
|
|
687
1255
|
except Exception as e:
|
|
688
1256
|
logger.warning(f"Failed to preload {name}: {e}")
|
|
689
1257
|
|
|
690
|
-
def get_stats(self) ->
|
|
1258
|
+
def get_stats(self) -> dict[str, Any]:
|
|
691
1259
|
"""Get loading statistics."""
|
|
692
1260
|
with self._lock:
|
|
693
1261
|
loaded_count = sum(
|
|
@@ -717,8 +1285,14 @@ class LazyModuleRegistry:
|
|
|
717
1285
|
class LazyImporter:
|
|
718
1286
|
"""
|
|
719
1287
|
Lazy importer that defers heavy module imports until first access.
|
|
1288
|
+
|
|
720
1289
|
Supports multiple load modes: NONE, AUTO, PRELOAD, BACKGROUND, CACHED,
|
|
721
1290
|
TURBO, ADAPTIVE, HYPERPARALLEL, STREAMING, ULTRA, INTELLIGENT.
|
|
1291
|
+
|
|
1292
|
+
ARCHITECTURAL NOTE: This class implements many modes for what should be a simple
|
|
1293
|
+
lazy loading mechanism. Modes like TURBO, ULTRA, and HYPERPARALLEL add significant
|
|
1294
|
+
complexity and may introduce concurrency issues with Python's import lock.
|
|
1295
|
+
Consider simplifying to just Lazy/Eager modes in future refactoring.
|
|
722
1296
|
"""
|
|
723
1297
|
|
|
724
1298
|
__slots__ = (
|
|
@@ -733,11 +1307,11 @@ class LazyImporter:
|
|
|
733
1307
|
"""Initialize lazy importer."""
|
|
734
1308
|
self._enabled = False
|
|
735
1309
|
self._load_mode = LazyLoadMode.NONE
|
|
736
|
-
self._lazy_modules:
|
|
737
|
-
self._loaded_modules:
|
|
738
|
-
self._access_counts:
|
|
739
|
-
self._load_times:
|
|
740
|
-
self._background_tasks:
|
|
1310
|
+
self._lazy_modules: dict[str, str] = {}
|
|
1311
|
+
self._loaded_modules: dict[str, ModuleType] = {}
|
|
1312
|
+
self._access_counts: dict[str, int] = {}
|
|
1313
|
+
self._load_times: dict[str, float] = {}
|
|
1314
|
+
self._background_tasks: dict[str, asyncio.Task] = {}
|
|
741
1315
|
self._async_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
742
1316
|
|
|
743
1317
|
# Superior mode components
|
|
@@ -1264,7 +1838,7 @@ class LazyImporter:
|
|
|
1264
1838
|
logger.error(f"Failed to preload {module_name}: {e}")
|
|
1265
1839
|
return False
|
|
1266
1840
|
|
|
1267
|
-
def get_stats(self) ->
|
|
1841
|
+
def get_stats(self) -> dict[str, Any]:
|
|
1268
1842
|
"""Get lazy import statistics."""
|
|
1269
1843
|
with self._lock:
|
|
1270
1844
|
return {
|
|
@@ -1321,7 +1895,7 @@ class LazyImportHook(AModuleHelper):
|
|
|
1321
1895
|
# =============================================================================
|
|
1322
1896
|
|
|
1323
1897
|
# Wrapped class cache
|
|
1324
|
-
_WRAPPED_CLASS_CACHE:
|
|
1898
|
+
_WRAPPED_CLASS_CACHE: dict[str, set[str]] = defaultdict(set)
|
|
1325
1899
|
_wrapped_cache_lock = threading.RLock()
|
|
1326
1900
|
|
|
1327
1901
|
# Default lazy methods
|
|
@@ -1333,15 +1907,15 @@ _DEFAULT_LAZY_METHODS = tuple(
|
|
|
1333
1907
|
)
|
|
1334
1908
|
|
|
1335
1909
|
# Lazy prefix method registry
|
|
1336
|
-
_lazy_prefix_method_registry:
|
|
1910
|
+
_lazy_prefix_method_registry: dict[str, tuple[str, ...]] = {}
|
|
1337
1911
|
|
|
1338
1912
|
# Package class hints
|
|
1339
|
-
_package_class_hints:
|
|
1913
|
+
_package_class_hints: dict[str, tuple[str, ...]] = {}
|
|
1340
1914
|
_class_hint_lock = threading.RLock()
|
|
1341
1915
|
|
|
1342
1916
|
def _set_package_class_hints(package_name: str, hints: Iterable[str]) -> None:
|
|
1343
1917
|
"""Set class hints for a package."""
|
|
1344
|
-
normalized:
|
|
1918
|
+
normalized: tuple[str, ...] = tuple(
|
|
1345
1919
|
OrderedDict((hint.lower(), None) for hint in hints if hint).keys() # type: ignore[arg-type]
|
|
1346
1920
|
)
|
|
1347
1921
|
with _class_hint_lock:
|
|
@@ -1350,7 +1924,7 @@ def _set_package_class_hints(package_name: str, hints: Iterable[str]) -> None:
|
|
|
1350
1924
|
else:
|
|
1351
1925
|
_package_class_hints.pop(package_name, None)
|
|
1352
1926
|
|
|
1353
|
-
def _get_package_class_hints(package_name: str) ->
|
|
1927
|
+
def _get_package_class_hints(package_name: str) -> tuple[str, ...]:
|
|
1354
1928
|
"""Get class hints for a package."""
|
|
1355
1929
|
with _class_hint_lock:
|
|
1356
1930
|
return _package_class_hints.get(package_name, ())
|
|
@@ -1360,7 +1934,7 @@ def _clear_all_package_class_hints() -> None:
|
|
|
1360
1934
|
with _class_hint_lock:
|
|
1361
1935
|
_package_class_hints.clear()
|
|
1362
1936
|
|
|
1363
|
-
def register_lazy_module_methods(prefix: str, methods:
|
|
1937
|
+
def register_lazy_module_methods(prefix: str, methods: tuple[str, ...]) -> None:
|
|
1364
1938
|
"""Register method names to enhance for all classes under a module prefix."""
|
|
1365
1939
|
prefix = prefix.strip()
|
|
1366
1940
|
if not prefix:
|
|
@@ -1472,39 +2046,45 @@ class LazyMetaPathFinder:
|
|
|
1472
2046
|
|
|
1473
2047
|
PERFORMANCE: Optimized for zero overhead on successful imports.
|
|
1474
2048
|
"""
|
|
1475
|
-
# Debug logging for msgpack to trace why it's not being intercepted
|
|
1476
|
-
if fullname == 'msgpack':
|
|
1477
|
-
logger.info(f"[HOOK] find_spec called for msgpack, enabled={self._enabled}, in_sys_modules={fullname in sys.modules}, installing={getattr(_installing_state, 'active', False)}, importing={getattr(_importing_state, 'active', False)}")
|
|
1478
|
-
|
|
1479
2049
|
# CRITICAL: Check installing state FIRST to prevent recursion during installation
|
|
1480
2050
|
if getattr(_installing_state, 'active', False):
|
|
1481
|
-
if fullname == 'msgpack':
|
|
1482
|
-
logger.info(f"[HOOK] Installation in progress, skipping msgpack")
|
|
1483
2051
|
logger.debug(f"[HOOK] Installation in progress, skipping {fullname} to prevent recursion")
|
|
1484
2052
|
return None
|
|
1485
2053
|
|
|
2054
|
+
# CRITICAL: Check if we're checking installation status to prevent infinite recursion
|
|
2055
|
+
if _is_checking_installation():
|
|
2056
|
+
logger.debug(f"[HOOK] Checking installation status, bypassing lazy finder for {fullname} to prevent recursion")
|
|
2057
|
+
return None
|
|
2058
|
+
|
|
1486
2059
|
# Fast path 1: Hook disabled
|
|
1487
2060
|
if not self._enabled:
|
|
1488
|
-
if fullname == 'msgpack':
|
|
1489
|
-
logger.info(f"[HOOK] Hook disabled, skipping msgpack")
|
|
1490
2061
|
return None
|
|
1491
2062
|
|
|
1492
|
-
# Fast path 2: Module already loaded
|
|
2063
|
+
# Fast path 2: Module already loaded - wrap it if needed
|
|
1493
2064
|
if fullname in sys.modules:
|
|
1494
|
-
|
|
1495
|
-
|
|
2065
|
+
module = sys.modules[fullname]
|
|
2066
|
+
# Wrap classes in already-loaded modules to ensure auto-instantiation works
|
|
2067
|
+
if isinstance(module, ModuleType) and not getattr(module, '_xwlazy_wrapped', False):
|
|
2068
|
+
try:
|
|
2069
|
+
self._wrap_classes_for_auto_instantiation(module)
|
|
2070
|
+
module._xwlazy_wrapped = True # Mark as wrapped to avoid re-wrapping
|
|
2071
|
+
except Exception:
|
|
2072
|
+
pass
|
|
1496
2073
|
return None
|
|
1497
2074
|
|
|
1498
2075
|
# Fast path 3: Skip C extension modules and internal modules
|
|
1499
|
-
|
|
2076
|
+
# Also skip submodules that start with underscore (e.g., yaml._yaml)
|
|
2077
|
+
if fullname.startswith('_') or ('.' in fullname and fullname.split('.')[-1].startswith('_')):
|
|
1500
2078
|
logger.debug(f"[HOOK] Skipping C extension/internal module {fullname}")
|
|
1501
2079
|
return None
|
|
1502
2080
|
|
|
1503
2081
|
# Fast path 4: Check if parent package is partially initialized
|
|
2082
|
+
# CRITICAL: Skip ALL submodules of packages that are in sys.modules
|
|
2083
|
+
# This prevents circular import issues when a package imports its own submodules
|
|
1504
2084
|
if '.' in fullname:
|
|
1505
2085
|
parent_package = fullname.split('.', 1)[0]
|
|
1506
2086
|
if parent_package in sys.modules:
|
|
1507
|
-
logger.debug(f"[HOOK] Skipping {fullname} - parent {parent_package} is
|
|
2087
|
+
logger.debug(f"[HOOK] Skipping {fullname} - parent {parent_package} is in sys.modules (prevent circular import)")
|
|
1508
2088
|
return None
|
|
1509
2089
|
if _is_import_in_progress(parent_package):
|
|
1510
2090
|
logger.debug(f"[HOOK] Skipping {fullname} - parent {parent_package} import in progress")
|
|
@@ -1528,7 +2108,22 @@ class LazyMetaPathFinder:
|
|
|
1528
2108
|
try:
|
|
1529
2109
|
installer = LazyInstallerRegistry.get_instance(self._package_name)
|
|
1530
2110
|
package_name = installer._dependency_mapper.get_package_name(root_name)
|
|
1531
|
-
|
|
2111
|
+
|
|
2112
|
+
# ROOT CAUSE FIX: If this is the package we are managing, DO NOT skip interception!
|
|
2113
|
+
# We need to wrap it even if it is installed, so we can intercept its imports.
|
|
2114
|
+
should_skip = False
|
|
2115
|
+
if package_name == self._package_name or root_name == self._package_name:
|
|
2116
|
+
should_skip = False
|
|
2117
|
+
elif package_name:
|
|
2118
|
+
# Set flag to prevent recursion during installation check
|
|
2119
|
+
_set_checking_installation(True)
|
|
2120
|
+
try:
|
|
2121
|
+
if installer.is_package_installed(package_name):
|
|
2122
|
+
should_skip = True
|
|
2123
|
+
finally:
|
|
2124
|
+
_set_checking_installation(False)
|
|
2125
|
+
|
|
2126
|
+
if should_skip:
|
|
1532
2127
|
logger.debug(f"[HOOK] Package {package_name} is installed (cache check), skipping interception of {fullname}")
|
|
1533
2128
|
return None
|
|
1534
2129
|
except Exception:
|
|
@@ -1550,25 +2145,24 @@ class LazyMetaPathFinder:
|
|
|
1550
2145
|
return None
|
|
1551
2146
|
|
|
1552
2147
|
# Fast path 6: Import in progress
|
|
2148
|
+
# NOTE: We allow lazy install to proceed even if import is in progress,
|
|
2149
|
+
# because we need to install missing packages during import
|
|
1553
2150
|
if _is_import_in_progress(fullname):
|
|
1554
|
-
if
|
|
1555
|
-
|
|
1556
|
-
|
|
2151
|
+
# Only skip if lazy install is disabled (for watched modules, we still need to wrap)
|
|
2152
|
+
if not lazy_install_enabled and not _watched_registry.has_root(root_name):
|
|
2153
|
+
return None
|
|
1557
2154
|
|
|
2155
|
+
# Only skip global importing state if lazy install is disabled
|
|
2156
|
+
# (lazy install needs to run even during imports to install missing packages)
|
|
1558
2157
|
if getattr(_importing_state, 'active', False):
|
|
1559
|
-
if
|
|
1560
|
-
|
|
1561
|
-
return None
|
|
2158
|
+
if not lazy_install_enabled and not _watched_registry.has_root(root_name):
|
|
2159
|
+
return None
|
|
1562
2160
|
|
|
1563
2161
|
# Install mode check already done above
|
|
1564
|
-
matching_prefixes:
|
|
2162
|
+
matching_prefixes: tuple[str, ...] = ()
|
|
1565
2163
|
if _watched_registry.has_root(root_name):
|
|
1566
2164
|
matching_prefixes = _watched_registry.get_matching_prefixes(fullname)
|
|
1567
2165
|
|
|
1568
|
-
# Debug: Check if msgpack reaches this point
|
|
1569
|
-
if fullname == 'msgpack':
|
|
1570
|
-
logger.debug(f"[HOOK] msgpack: matching_prefixes={matching_prefixes}, has_root={_watched_registry.has_root(root_name)}")
|
|
1571
|
-
|
|
1572
2166
|
installer = LazyInstallerRegistry.get_instance(self._package_name)
|
|
1573
2167
|
|
|
1574
2168
|
# Two-stage lazy loading for serialization and archive modules
|
|
@@ -1608,6 +2202,8 @@ class LazyMetaPathFinder:
|
|
|
1608
2202
|
if module:
|
|
1609
2203
|
try:
|
|
1610
2204
|
self._enhance_classes_with_class_methods(module)
|
|
2205
|
+
# Enable auto-instantiation for classes in this module
|
|
2206
|
+
self._wrap_classes_for_auto_instantiation(module)
|
|
1611
2207
|
except Exception as enhance_exc:
|
|
1612
2208
|
logger.debug(f"[HOOK] Could not enhance classes in {fullname}: {enhance_exc}")
|
|
1613
2209
|
spec = _spec_for_existing_module(fullname, module, spec)
|
|
@@ -1649,9 +2245,15 @@ class LazyMetaPathFinder:
|
|
|
1649
2245
|
try:
|
|
1650
2246
|
installer = LazyInstallerRegistry.get_instance(self._package_name)
|
|
1651
2247
|
package_name = installer._dependency_mapper.get_package_name(parent_package)
|
|
1652
|
-
if package_name
|
|
1653
|
-
|
|
1654
|
-
|
|
2248
|
+
if package_name:
|
|
2249
|
+
# Set flag to prevent recursion during installation check
|
|
2250
|
+
_set_checking_installation(True)
|
|
2251
|
+
try:
|
|
2252
|
+
if not installer.is_package_installed(package_name):
|
|
2253
|
+
logger.debug(f"[HOOK] Parent package {parent_package} not installed, intercepting parent")
|
|
2254
|
+
return self.find_spec(parent_package, path, target)
|
|
2255
|
+
finally:
|
|
2256
|
+
_set_checking_installation(False)
|
|
1655
2257
|
except Exception:
|
|
1656
2258
|
pass
|
|
1657
2259
|
return None
|
|
@@ -1663,9 +2265,63 @@ class LazyMetaPathFinder:
|
|
|
1663
2265
|
# ROOT CAUSE FIX: For lazy installation, intercept missing imports and install them
|
|
1664
2266
|
logger.debug(f"[HOOK] Checking lazy install for {fullname}: enabled={lazy_install_enabled}, install_mode={install_mode}")
|
|
1665
2267
|
if lazy_install_enabled:
|
|
2268
|
+
# Prevent infinite loops: Skip if already attempting import
|
|
1666
2269
|
if _is_import_in_progress(fullname):
|
|
2270
|
+
logger.debug(f"[HOOK] Import {fullname} already in progress, skipping to prevent recursion")
|
|
1667
2271
|
return None
|
|
1668
2272
|
|
|
2273
|
+
# Prevent infinite loops: Check if we've already tried to install this package
|
|
2274
|
+
installer = LazyInstallerRegistry.get_instance(self._package_name)
|
|
2275
|
+
package_name = installer._dependency_mapper.get_package_name(root_name)
|
|
2276
|
+
|
|
2277
|
+
# ROOT CAUSE FIX: Check if module is ALREADY importable BEFORE checking package installation
|
|
2278
|
+
# This handles cases where package name != module name (e.g., PyYAML -> yaml)
|
|
2279
|
+
try:
|
|
2280
|
+
# Try direct import first (most reliable check)
|
|
2281
|
+
if fullname in sys.modules:
|
|
2282
|
+
logger.debug(f"[HOOK] Module {fullname} already in sys.modules, skipping installation")
|
|
2283
|
+
return None
|
|
2284
|
+
|
|
2285
|
+
# Temporarily remove finders to check actual importability
|
|
2286
|
+
xwlazy_finder_names = {'LazyMetaPathFinder', 'LazyPathFinder', 'LazyLoader'}
|
|
2287
|
+
xwlazy_finders = [f for f in sys.meta_path if type(f).__name__ in xwlazy_finder_names]
|
|
2288
|
+
for finder in xwlazy_finders:
|
|
2289
|
+
try:
|
|
2290
|
+
sys.meta_path.remove(finder)
|
|
2291
|
+
except ValueError:
|
|
2292
|
+
pass
|
|
2293
|
+
|
|
2294
|
+
try:
|
|
2295
|
+
# Check spec without importing (avoids triggering module code execution)
|
|
2296
|
+
# This prevents circular import issues when checking importability
|
|
2297
|
+
spec = importlib.util.find_spec(fullname)
|
|
2298
|
+
if spec is not None and spec.loader is not None:
|
|
2299
|
+
logger.debug(f"[HOOK] Module {fullname} has valid spec, skipping installation")
|
|
2300
|
+
return None
|
|
2301
|
+
finally:
|
|
2302
|
+
# Restore finders
|
|
2303
|
+
for finder in reversed(xwlazy_finders):
|
|
2304
|
+
if finder not in sys.meta_path:
|
|
2305
|
+
sys.meta_path.insert(0, finder)
|
|
2306
|
+
except Exception as e:
|
|
2307
|
+
logger.debug(f"[HOOK] Importability check failed for {fullname}: {e}")
|
|
2308
|
+
# If check fails, proceed with installation attempt
|
|
2309
|
+
|
|
2310
|
+
if package_name:
|
|
2311
|
+
if package_name in installer.get_failed_packages():
|
|
2312
|
+
logger.debug(f"[HOOK] Package {package_name} previously failed installation, skipping {fullname}")
|
|
2313
|
+
return None
|
|
2314
|
+
|
|
2315
|
+
# Also check package installation status (for cache efficiency)
|
|
2316
|
+
# Set flag to prevent recursion during installation check
|
|
2317
|
+
_set_checking_installation(True)
|
|
2318
|
+
try:
|
|
2319
|
+
if installer.is_package_installed(package_name):
|
|
2320
|
+
logger.debug(f"[HOOK] Package {package_name} is already installed, skipping installation attempt for {fullname}")
|
|
2321
|
+
return None
|
|
2322
|
+
finally:
|
|
2323
|
+
_set_checking_installation(False)
|
|
2324
|
+
|
|
1669
2325
|
_mark_import_started(fullname)
|
|
1670
2326
|
try:
|
|
1671
2327
|
# Guard against recursion when importing facade
|
|
@@ -1677,6 +2333,12 @@ class LazyMetaPathFinder:
|
|
|
1677
2333
|
return None
|
|
1678
2334
|
from ..facade import lazy_import_with_install
|
|
1679
2335
|
|
|
2336
|
+
# Log installation attempt (DEBUG level to reduce noise)
|
|
2337
|
+
if package_name:
|
|
2338
|
+
logger.debug(f"⏳ [HOOK] Attempting to install package '{package_name}' for module '{fullname}'")
|
|
2339
|
+
else:
|
|
2340
|
+
logger.debug(f"⏳ [HOOK] Attempting to install module '{fullname}' (no package mapping found)")
|
|
2341
|
+
|
|
1680
2342
|
_importing_state.active = True
|
|
1681
2343
|
try:
|
|
1682
2344
|
module, success = lazy_import_with_install(
|
|
@@ -1688,6 +2350,7 @@ class LazyMetaPathFinder:
|
|
|
1688
2350
|
|
|
1689
2351
|
if success and module:
|
|
1690
2352
|
# Module was successfully installed and imported
|
|
2353
|
+
logger.debug(f"✅ [HOOK] Successfully installed and imported '{fullname}'")
|
|
1691
2354
|
xwlazy_finder_names = {'LazyMetaPathFinder', 'LazyPathFinder', 'LazyLoader'}
|
|
1692
2355
|
xwlazy_finders = [f for f in sys.meta_path if type(f).__name__ in xwlazy_finder_names]
|
|
1693
2356
|
for finder in xwlazy_finders:
|
|
@@ -1710,18 +2373,35 @@ class LazyMetaPathFinder:
|
|
|
1710
2373
|
sys.meta_path.insert(0, finder)
|
|
1711
2374
|
return None
|
|
1712
2375
|
else:
|
|
1713
|
-
|
|
2376
|
+
# Only log warning if it's not a known missing package (reduce noise)
|
|
2377
|
+
if package_name and package_name not in installer.get_failed_packages():
|
|
2378
|
+
logger.debug(f"❌ [HOOK] Failed to install/import {fullname}")
|
|
2379
|
+
else:
|
|
2380
|
+
logger.debug(f"❌ [HOOK] Failed to install/import {fullname} (already marked as failed)")
|
|
2381
|
+
# Mark as failed to prevent infinite retry loops
|
|
2382
|
+
if package_name:
|
|
2383
|
+
installer._failed_packages.add(package_name)
|
|
1714
2384
|
try:
|
|
1715
2385
|
installer = LazyInstallerRegistry.get_instance(self._package_name)
|
|
1716
|
-
|
|
2386
|
+
# Force disable async install usage in engine
|
|
2387
|
+
use_async = False # installer.is_async_enabled()
|
|
2388
|
+
if use_async:
|
|
1717
2389
|
placeholder = self._build_async_placeholder(fullname, installer)
|
|
1718
2390
|
if placeholder is not None:
|
|
1719
2391
|
return placeholder
|
|
1720
2392
|
except Exception:
|
|
1721
2393
|
pass
|
|
2394
|
+
# Return None to let Python handle the ImportError naturally
|
|
1722
2395
|
return None
|
|
1723
2396
|
except Exception as e:
|
|
1724
|
-
logger.
|
|
2397
|
+
logger.error(f"❌ [HOOK] Lazy import hook failed for {fullname}: {e}", exc_info=True)
|
|
2398
|
+
# Mark as failed to prevent infinite retry loops
|
|
2399
|
+
if package_name:
|
|
2400
|
+
try:
|
|
2401
|
+
installer = LazyInstallerRegistry.get_instance(self._package_name)
|
|
2402
|
+
installer._failed_packages.add(package_name)
|
|
2403
|
+
except Exception:
|
|
2404
|
+
pass
|
|
1725
2405
|
return None
|
|
1726
2406
|
finally:
|
|
1727
2407
|
_mark_import_finished(fullname)
|
|
@@ -1787,6 +2467,11 @@ class LazyMetaPathFinder:
|
|
|
1787
2467
|
logger.debug(f"[STAGE 1] Async install scheduling failed for '{name}': {schedule_exc}")
|
|
1788
2468
|
deferred = DeferredImportError(name, e, self._package_name, async_handle=async_handle)
|
|
1789
2469
|
deferred_imports[name] = deferred
|
|
2470
|
+
|
|
2471
|
+
# ROOT CAUSE FIX: Register deferred object in sys.modules to prevent infinite import loops
|
|
2472
|
+
# If we don't do this, subsequent imports of the same missing module will trigger find_spec again
|
|
2473
|
+
sys.modules[name] = deferred
|
|
2474
|
+
|
|
1790
2475
|
return deferred
|
|
1791
2476
|
finally:
|
|
1792
2477
|
_mark_import_finished(name)
|
|
@@ -1806,6 +2491,9 @@ class LazyMetaPathFinder:
|
|
|
1806
2491
|
|
|
1807
2492
|
self._enhance_classes_with_class_methods(module)
|
|
1808
2493
|
|
|
2494
|
+
# Enable auto-instantiation for classes in this module
|
|
2495
|
+
self._wrap_classes_for_auto_instantiation(module)
|
|
2496
|
+
|
|
1809
2497
|
finally:
|
|
1810
2498
|
logger.debug(f"[STAGE 1] Restoring original __import__")
|
|
1811
2499
|
builtins.__import__ = original_import
|
|
@@ -1852,7 +2540,7 @@ class LazyMetaPathFinder:
|
|
|
1852
2540
|
return
|
|
1853
2541
|
dep_entries = [(lower, deferred_imports[lower_map[lower]]) for lower in pending_lower]
|
|
1854
2542
|
wrapped_count = 0
|
|
1855
|
-
newly_wrapped:
|
|
2543
|
+
newly_wrapped: set[str] = set()
|
|
1856
2544
|
|
|
1857
2545
|
for name, obj in list(module.__dict__.items()):
|
|
1858
2546
|
if not pending_lower:
|
|
@@ -1890,88 +2578,94 @@ class LazyMetaPathFinder:
|
|
|
1890
2578
|
|
|
1891
2579
|
log_event("hook", logger.info, f"[STAGE 1] Wrapped {wrapped_count} classes in {module_name}")
|
|
1892
2580
|
|
|
1893
|
-
def
|
|
1894
|
-
"""
|
|
1895
|
-
|
|
1896
|
-
return
|
|
2581
|
+
def _wrap_classes_for_auto_instantiation(self, module: ModuleType) -> None:
|
|
2582
|
+
"""
|
|
2583
|
+
Wrap classes in modules with AutoInstantiateProxy for auto-instantiation.
|
|
1897
2584
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
if module.__name__.startswith(prefix.rstrip('.')):
|
|
1901
|
-
methods_to_apply = methods
|
|
1902
|
-
break
|
|
2585
|
+
This enables: from module import Class as instance
|
|
2586
|
+
Then: instance.method() works automatically without manual instantiation.
|
|
1903
2587
|
|
|
1904
|
-
|
|
1905
|
-
|
|
2588
|
+
We wrap classes directly in module.__dict__ AND add a __getattr__ fallback
|
|
2589
|
+
to catch any classes that might be accessed before wrapping completes.
|
|
1906
2590
|
|
|
1907
|
-
|
|
1908
|
-
|
|
2591
|
+
We wrap ALL classes in the module's namespace, whether defined in this
|
|
2592
|
+
module or imported from submodules (like BsonSerializer imported in __init__.py).
|
|
2593
|
+
"""
|
|
2594
|
+
import inspect
|
|
2595
|
+
import types
|
|
2596
|
+
|
|
2597
|
+
# Store original __getattr__ if it exists
|
|
2598
|
+
original_getattr = getattr(module, '__getattr__', None)
|
|
2599
|
+
|
|
2600
|
+
wrapped_count = 0
|
|
2601
|
+
wrapped_classes = {} # Track wrapped classes for __getattr__
|
|
1909
2602
|
|
|
1910
|
-
enhanced = 0
|
|
1911
2603
|
for name, obj in list(module.__dict__.items()):
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2604
|
+
# Wrap classes that are in this module's namespace
|
|
2605
|
+
# This includes classes defined here AND classes imported from submodules
|
|
2606
|
+
if (inspect.isclass(obj) and
|
|
2607
|
+
not inspect.isbuiltin(obj) and
|
|
2608
|
+
hasattr(obj, '__module__')):
|
|
2609
|
+
# Skip if it's already a proxy
|
|
2610
|
+
if isinstance(obj, AutoInstantiateProxy):
|
|
1917
2611
|
continue
|
|
1918
|
-
if
|
|
2612
|
+
# Skip if it's a wrapper class
|
|
2613
|
+
if obj.__name__.startswith('Lazy'):
|
|
1919
2614
|
continue
|
|
1920
|
-
if
|
|
2615
|
+
# Skip if it's a type from typing or other special modules
|
|
2616
|
+
module_name = obj.__module__
|
|
2617
|
+
if module_name in ('typing', 'builtins', '__builtin__'):
|
|
1921
2618
|
continue
|
|
1922
2619
|
|
|
1923
|
-
|
|
1924
|
-
|
|
2620
|
+
logger.debug(f"[AUTO-INST] Wrapping class '{name}' ({module_name}) in {module.__name__} for auto-instantiation")
|
|
2621
|
+
proxy = AutoInstantiateProxy(obj)
|
|
2622
|
+
module.__dict__[name] = proxy
|
|
2623
|
+
wrapped_classes[name] = proxy
|
|
2624
|
+
wrapped_count += 1
|
|
2625
|
+
|
|
2626
|
+
# Add __getattr__ fallback to wrap classes on access (for classes added after initial load)
|
|
2627
|
+
if wrapped_count > 0 or True: # Always add __getattr__ for consistency
|
|
2628
|
+
def auto_instantiate_getattr(name: str):
|
|
2629
|
+
"""Module-level __getattr__ that wraps classes for auto-instantiation."""
|
|
2630
|
+
# First try original __getattr__
|
|
2631
|
+
if original_getattr:
|
|
2632
|
+
try:
|
|
2633
|
+
attr = original_getattr(name)
|
|
2634
|
+
# If it's a class, wrap it
|
|
2635
|
+
if inspect.isclass(attr) and not inspect.isbuiltin(attr):
|
|
2636
|
+
if not isinstance(attr, AutoInstantiateProxy):
|
|
2637
|
+
logger.debug(f"[AUTO-INST] Wrapping class '{name}' on access in {module.__name__}")
|
|
2638
|
+
proxy = AutoInstantiateProxy(attr)
|
|
2639
|
+
module.__dict__[name] = proxy # Cache it
|
|
2640
|
+
return proxy
|
|
2641
|
+
return attr
|
|
2642
|
+
except AttributeError:
|
|
2643
|
+
pass
|
|
1925
2644
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
if
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2645
|
+
# Get from __dict__ (might be unwrapped)
|
|
2646
|
+
if name in module.__dict__:
|
|
2647
|
+
attr = module.__dict__[name]
|
|
2648
|
+
# If it's an unwrapped class, wrap it now
|
|
2649
|
+
if (inspect.isclass(attr) and
|
|
2650
|
+
not inspect.isbuiltin(attr) and
|
|
2651
|
+
not isinstance(attr, AutoInstantiateProxy) and
|
|
2652
|
+
hasattr(attr, '__module__')):
|
|
2653
|
+
module_name = attr.__module__
|
|
2654
|
+
if module_name not in ('typing', 'builtins', '__builtin__'):
|
|
2655
|
+
logger.debug(f"[AUTO-INST] Wrapping class '{name}' on access in {module.__name__}")
|
|
2656
|
+
proxy = AutoInstantiateProxy(attr)
|
|
2657
|
+
module.__dict__[name] = proxy # Cache it
|
|
2658
|
+
return proxy
|
|
2659
|
+
return attr
|
|
1939
2660
|
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
instance = cls()
|
|
1946
|
-
return func(instance, *args, **kwargs)
|
|
1947
|
-
_class_call.__name__ = getattr(func, '__name__', 'lazy_method')
|
|
1948
|
-
_class_call.__doc__ = func.__doc__
|
|
1949
|
-
_class_call.__lazy_wrapped__ = True
|
|
1950
|
-
return _class_call
|
|
1951
|
-
|
|
1952
|
-
setattr(
|
|
1953
|
-
obj,
|
|
1954
|
-
method_name,
|
|
1955
|
-
classmethod(class_method_wrapper(original_func)),
|
|
1956
|
-
)
|
|
1957
|
-
enhanced += 1
|
|
1958
|
-
logger.debug(
|
|
1959
|
-
"[LAZY ENHANCE] Added class-level %s() to %s.%s",
|
|
1960
|
-
method_name,
|
|
1961
|
-
module.__name__,
|
|
1962
|
-
name,
|
|
1963
|
-
)
|
|
1964
|
-
except Exception as exc:
|
|
1965
|
-
logger.debug(
|
|
1966
|
-
"[LAZY ENHANCE] Skipped %s.%s.%s: %s",
|
|
1967
|
-
module.__name__,
|
|
1968
|
-
name,
|
|
1969
|
-
method_name,
|
|
1970
|
-
exc,
|
|
1971
|
-
)
|
|
2661
|
+
raise AttributeError(f"module '{module.__name__}' has no attribute '{name}'")
|
|
2662
|
+
|
|
2663
|
+
# Only set __getattr__ if module doesn't already have a custom one
|
|
2664
|
+
if not hasattr(module, '__getattr__') or isinstance(getattr(module, '__getattr__', None), types.MethodType):
|
|
2665
|
+
module.__getattr__ = auto_instantiate_getattr # type: ignore[attr-defined]
|
|
1972
2666
|
|
|
1973
|
-
if
|
|
1974
|
-
|
|
2667
|
+
if wrapped_count > 0:
|
|
2668
|
+
logger.debug(f"[AUTO-INST] Wrapped {wrapped_count} classes in {module.__name__} for auto-instantiation")
|
|
1975
2669
|
|
|
1976
2670
|
def _create_lazy_class_wrapper(self, original_class, deferred_import: DeferredImportError):
|
|
1977
2671
|
"""Create a wrapper class that installs dependencies when instantiated."""
|
|
@@ -1998,9 +2692,155 @@ class LazyMetaPathFinder:
|
|
|
1998
2692
|
LazyClassWrapper.__doc__ = original_class.__doc__
|
|
1999
2693
|
|
|
2000
2694
|
return LazyClassWrapper
|
|
2695
|
+
|
|
2696
|
+
def _enhance_classes_with_class_methods(self, module):
|
|
2697
|
+
"""Enhance classes with lazy class methods - automatically detects ALL instance methods."""
|
|
2698
|
+
if module is None:
|
|
2699
|
+
return
|
|
2700
|
+
# Debug: Check if module has classes
|
|
2701
|
+
module_classes = [name for name, obj in module.__dict__.items() if isinstance(obj, type)]
|
|
2702
|
+
if not module_classes:
|
|
2703
|
+
return # No classes to enhance
|
|
2704
|
+
|
|
2705
|
+
# Get methods from registry (if any) for specific prefixes
|
|
2706
|
+
methods_to_apply: tuple[str, ...] = ()
|
|
2707
|
+
for prefix, methods in _lazy_prefix_method_registry.items():
|
|
2708
|
+
if module.__name__.startswith(prefix.rstrip('.')):
|
|
2709
|
+
methods_to_apply = methods
|
|
2710
|
+
break
|
|
2711
|
+
|
|
2712
|
+
# If no prefix match, use default methods from env var
|
|
2713
|
+
if not methods_to_apply:
|
|
2714
|
+
methods_to_apply = _DEFAULT_LAZY_METHODS
|
|
2715
|
+
|
|
2716
|
+
# CRITICAL FIX: If no specific methods registered, enhance ALL instance methods
|
|
2717
|
+
# This makes xwlazy handle ALL scenarios (classes, functions, instance methods)
|
|
2718
|
+
auto_detect_all = not methods_to_apply
|
|
2719
|
+
|
|
2720
|
+
enhanced = 0
|
|
2721
|
+
for name, obj in list(module.__dict__.items()):
|
|
2722
|
+
if not isinstance(obj, type):
|
|
2723
|
+
continue
|
|
2724
|
+
|
|
2725
|
+
# Get methods to enhance for this class
|
|
2726
|
+
if auto_detect_all:
|
|
2727
|
+
# Auto-detect instance methods defined DIRECTLY in this class (not inherited)
|
|
2728
|
+
# This prevents wrapping wrong methods from parent classes
|
|
2729
|
+
methods_to_wrap = []
|
|
2730
|
+
# Only check methods in this class's __dict__ to avoid inherited methods
|
|
2731
|
+
for attr_name, attr_value in obj.__dict__.items():
|
|
2732
|
+
if attr_name.startswith('_') and attr_name not in ('__init__', '__new__'):
|
|
2733
|
+
continue # Skip private methods except __init__ and __new__
|
|
2734
|
+
if not callable(attr_value):
|
|
2735
|
+
continue
|
|
2736
|
+
if isinstance(attr_value, (classmethod, staticmethod)):
|
|
2737
|
+
continue
|
|
2738
|
+
if getattr(attr_value, "__lazy_wrapped__", False):
|
|
2739
|
+
continue
|
|
2740
|
+
# Check if it's an instance method (has 'self' as first param)
|
|
2741
|
+
import inspect
|
|
2742
|
+
try:
|
|
2743
|
+
if inspect.isfunction(attr_value):
|
|
2744
|
+
sig = inspect.signature(attr_value)
|
|
2745
|
+
params = list(sig.parameters.keys())
|
|
2746
|
+
if params and params[0] == 'self':
|
|
2747
|
+
methods_to_wrap.append(attr_name)
|
|
2748
|
+
except (ValueError, TypeError):
|
|
2749
|
+
# Can't inspect signature, skip
|
|
2750
|
+
pass
|
|
2751
|
+
else:
|
|
2752
|
+
# Use specific methods from registry
|
|
2753
|
+
methods_to_wrap = list(methods_to_apply)
|
|
2754
|
+
|
|
2755
|
+
# Enhance each method
|
|
2756
|
+
for method_name in methods_to_wrap:
|
|
2757
|
+
try:
|
|
2758
|
+
# CRITICAL FIX: Only get from class __dict__ to ensure we get the RIGHT method
|
|
2759
|
+
original_func = obj.__dict__.get(method_name)
|
|
2760
|
+
if original_func is None:
|
|
2761
|
+
continue # Method not in class dict
|
|
2762
|
+
if not inspect.isfunction(original_func):
|
|
2763
|
+
continue
|
|
2764
|
+
if getattr(original_func, "__lazy_wrapped__", False):
|
|
2765
|
+
continue
|
|
2766
|
+
if isinstance(original_func, (classmethod, staticmethod)):
|
|
2767
|
+
continue
|
|
2768
|
+
if original_func.__name__ != method_name:
|
|
2769
|
+
continue
|
|
2770
|
+
# Verify it's an instance method
|
|
2771
|
+
try:
|
|
2772
|
+
params = list(inspect.signature(original_func).parameters.keys())
|
|
2773
|
+
if not params or params[0] != 'self':
|
|
2774
|
+
continue
|
|
2775
|
+
except Exception:
|
|
2776
|
+
continue
|
|
2777
|
+
# Create wrapper with proper closure
|
|
2778
|
+
class_obj, func_to_call = obj, original_func
|
|
2779
|
+
import functools
|
|
2780
|
+
def make_wrapper(fn, cls):
|
|
2781
|
+
@functools.wraps(fn)
|
|
2782
|
+
def wrapper(first_arg, *args, **kwargs):
|
|
2783
|
+
if isinstance(first_arg, cls):
|
|
2784
|
+
return fn(first_arg, *args, **kwargs)
|
|
2785
|
+
instance = cls()
|
|
2786
|
+
return fn(instance, first_arg, *args, **kwargs)
|
|
2787
|
+
return wrapper
|
|
2788
|
+
smart_wrapper = make_wrapper(func_to_call, class_obj)
|
|
2789
|
+
smart_wrapper.__lazy_wrapped__ = True
|
|
2790
|
+
setattr(obj, method_name, smart_wrapper)
|
|
2791
|
+
enhanced += 1
|
|
2792
|
+
except Exception as exc:
|
|
2793
|
+
# Silent skip - one line as requested (package/module agnostic)
|
|
2794
|
+
pass
|
|
2795
|
+
|
|
2796
|
+
if enhanced:
|
|
2797
|
+
log_event("enhance", logger.info, "✓ [LAZY ENHANCE] Enhanced %s methods in %s", enhanced, module.__name__)
|
|
2798
|
+
|
|
2799
|
+
|
|
2800
|
+
class AutoInstantiateProxy:
|
|
2801
|
+
"""
|
|
2802
|
+
Proxy that auto-instantiates a class on first method call.
|
|
2803
|
+
|
|
2804
|
+
Enables: from module import Class as instance
|
|
2805
|
+
Then: instance.method() automatically creates Class() and calls method
|
|
2806
|
+
|
|
2807
|
+
This allows users to import classes with 'as' and use them directly
|
|
2808
|
+
without needing to manually instantiate.
|
|
2809
|
+
"""
|
|
2810
|
+
|
|
2811
|
+
def __init__(self, original_class):
|
|
2812
|
+
"""Initialize proxy with the class to wrap."""
|
|
2813
|
+
self._class = original_class
|
|
2814
|
+
self._instance = None
|
|
2815
|
+
self._is_instantiated = False
|
|
2816
|
+
|
|
2817
|
+
def _ensure_instantiated(self):
|
|
2818
|
+
"""Ensure the class is instantiated."""
|
|
2819
|
+
if not self._is_instantiated:
|
|
2820
|
+
self._instance = self._class()
|
|
2821
|
+
self._is_instantiated = True
|
|
2822
|
+
|
|
2823
|
+
def __getattr__(self, name: str):
|
|
2824
|
+
"""Get attribute from instance, instantiating if needed."""
|
|
2825
|
+
self._ensure_instantiated()
|
|
2826
|
+
return getattr(self._instance, name)
|
|
2827
|
+
|
|
2828
|
+
def __call__(self, *args, **kwargs):
|
|
2829
|
+
"""Allow calling the proxy directly (creates new instance)."""
|
|
2830
|
+
return self._class(*args, **kwargs)
|
|
2831
|
+
|
|
2832
|
+
def __repr__(self):
|
|
2833
|
+
if self._is_instantiated:
|
|
2834
|
+
return f"<AutoInstantiateProxy({self._class.__name__}): instantiated>"
|
|
2835
|
+
return f"<AutoInstantiateProxy({self._class.__name__}): will instantiate on first use>"
|
|
2836
|
+
|
|
2837
|
+
@property
|
|
2838
|
+
def __class__(self):
|
|
2839
|
+
"""Return the wrapped class for isinstance checks."""
|
|
2840
|
+
return self._class
|
|
2001
2841
|
|
|
2002
2842
|
# Registry of installed hooks per package
|
|
2003
|
-
_installed_hooks:
|
|
2843
|
+
_installed_hooks: dict[str, LazyMetaPathFinder] = {}
|
|
2004
2844
|
_hook_lock = threading.RLock()
|
|
2005
2845
|
|
|
2006
2846
|
def install_import_hook(package_name: str = 'default') -> None:
|
|
@@ -2113,5 +2953,12 @@ __all__ = [
|
|
|
2113
2953
|
'_get_package_class_hints',
|
|
2114
2954
|
'_clear_all_package_class_hints',
|
|
2115
2955
|
'_spec_for_existing_module',
|
|
2956
|
+
# Global __import__ hook (for module-level imports)
|
|
2957
|
+
'register_lazy_package',
|
|
2958
|
+
'install_global_import_hook',
|
|
2959
|
+
'uninstall_global_import_hook',
|
|
2960
|
+
'is_global_import_hook_installed',
|
|
2961
|
+
'clear_global_import_caches',
|
|
2962
|
+
'get_global_import_cache_stats',
|
|
2116
2963
|
]
|
|
2117
2964
|
|