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.
Files changed (71) hide show
  1. exonware/__init__.py +22 -6
  2. exonware/xwlazy/__init__.py +14 -2
  3. exonware/xwlazy/common/__init__.py +8 -0
  4. exonware/xwlazy/common/base.py +11 -2
  5. exonware/xwlazy/common/cache.py +5 -5
  6. exonware/xwlazy/common/logger.py +5 -5
  7. exonware/xwlazy/common/services/dependency_mapper.py +31 -13
  8. exonware/xwlazy/common/services/install_async_utils.py +5 -0
  9. exonware/xwlazy/common/services/install_cache_utils.py +4 -4
  10. exonware/xwlazy/common/services/spec_cache.py +2 -2
  11. exonware/xwlazy/common/services/state_manager.py +4 -4
  12. exonware/xwlazy/common/strategies/caching_dict.py +2 -2
  13. exonware/xwlazy/common/strategies/caching_lfu.py +2 -2
  14. exonware/xwlazy/common/strategies/caching_ttl.py +2 -2
  15. exonware/xwlazy/common/utils.py +142 -0
  16. exonware/xwlazy/config.py +1 -1
  17. exonware/xwlazy/contracts.py +162 -25
  18. exonware/xwlazy/defs.py +15 -15
  19. exonware/xwlazy/facade.py +175 -29
  20. exonware/xwlazy/host/__init__.py +8 -0
  21. exonware/xwlazy/host/conf.py +16 -0
  22. exonware/xwlazy/module/base.py +61 -4
  23. exonware/xwlazy/module/facade.py +1 -1
  24. exonware/xwlazy/module/importer_engine.py +1017 -170
  25. exonware/xwlazy/module/partial_module_detector.py +275 -0
  26. exonware/xwlazy/module/strategies/module_helper_lazy.py +3 -3
  27. exonware/xwlazy/package/base.py +106 -41
  28. exonware/xwlazy/package/conf.py +6 -6
  29. exonware/xwlazy/package/services/config_manager.py +20 -16
  30. exonware/xwlazy/package/services/discovery.py +81 -16
  31. exonware/xwlazy/package/services/host_packages.py +41 -6
  32. exonware/xwlazy/package/services/install_async.py +16 -2
  33. exonware/xwlazy/package/services/install_cache.py +4 -4
  34. exonware/xwlazy/package/services/install_policy.py +14 -14
  35. exonware/xwlazy/package/services/install_registry.py +3 -3
  36. exonware/xwlazy/package/services/install_sbom.py +1 -1
  37. exonware/xwlazy/package/services/installer_engine.py +3 -3
  38. exonware/xwlazy/package/services/lazy_installer.py +102 -17
  39. exonware/xwlazy/package/services/manifest.py +43 -36
  40. exonware/xwlazy/package/services/strategy_registry.py +150 -12
  41. exonware/xwlazy/package/strategies/package_discovery_file.py +2 -2
  42. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +2 -2
  43. exonware/xwlazy/package/strategies/package_discovery_manifest.py +2 -2
  44. exonware/xwlazy/package/strategies/package_execution_async.py +3 -3
  45. exonware/xwlazy/package/strategies/package_execution_cached.py +2 -2
  46. exonware/xwlazy/package/strategies/package_execution_pip.py +2 -2
  47. exonware/xwlazy/package/strategies/package_execution_wheel.py +2 -2
  48. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +2 -2
  49. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +2 -2
  50. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +2 -2
  51. exonware/xwlazy/package/strategies/package_policy_allow_list.py +4 -4
  52. exonware/xwlazy/package/strategies/package_policy_deny_list.py +4 -4
  53. exonware/xwlazy/package/strategies/package_policy_permissive.py +3 -3
  54. exonware/xwlazy/package/strategies/package_timing_clean.py +2 -2
  55. exonware/xwlazy/package/strategies/package_timing_full.py +2 -2
  56. exonware/xwlazy/package/strategies/package_timing_smart.py +2 -2
  57. exonware/xwlazy/package/strategies/package_timing_temporary.py +2 -2
  58. exonware/xwlazy/runtime/adaptive_learner.py +7 -7
  59. exonware/xwlazy/runtime/base.py +14 -14
  60. exonware/xwlazy/runtime/facade.py +7 -7
  61. exonware/xwlazy/runtime/intelligent_selector.py +6 -6
  62. exonware/xwlazy/runtime/metrics.py +6 -6
  63. exonware/xwlazy/runtime/performance.py +5 -5
  64. exonware/xwlazy/version.py +2 -2
  65. {exonware_xwlazy-0.1.0.21.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/METADATA +2 -6
  66. exonware_xwlazy-0.1.0.23.dist-info/RECORD +93 -0
  67. xwlazy/__init__.py +14 -0
  68. xwlazy/lazy.py +30 -0
  69. exonware_xwlazy-0.1.0.21.dist-info/RECORD +0 -87
  70. {exonware_xwlazy-0.1.0.21.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/WHEEL +0 -0
  71. {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 Dict, List, Optional, Set, Tuple, Any, Iterable, Callable
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
- def _get_thread_imports() -> Set[str]:
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: Dict[str, Dict[str, Any]] = {}
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) -> Tuple[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: List[str] = []
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[List[str]] = None) -> None:
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: Dict[str, Set[str]] = {}
199
- self._prefixes: Set[str] = set()
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: Set[str] = set()
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) -> Tuple[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: List[str]) -> Dict[str, Any]:
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: Dict[str, Any] = {}
934
+ results: dict[str, Any] = {}
374
935
 
375
- def _load_module(module_path: str) -> Tuple[str, Any, Optional[Exception]]:
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: List[Tuple[str, int]]
394
- ) -> Dict[str, Any]:
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: Dict[str, List[str]] = {}
412
- self._reverse_deps: Dict[str, List[str]] = {}
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: List[str]) -> None:
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: List[str]) -> List[List[str]]:
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: Dict[str, int] = {m: 0 for m in modules}
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: List[List[str]] = []
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
- """Patch importlib.import_module to be lazy-aware."""
476
- importlib.import_module = _lazy_aware_import_module
477
- logger.debug("Patched importlib.import_module to be lazy-aware")
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 ...common.services import _detect_lazy_installation
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
- """Schedule lazy mode bootstrap to run AFTER the calling package finishes importing."""
552
- package_name_lower = package_name.lower()
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
- original_import = __builtins__.__import__ if hasattr(__builtins__, '__import__') else __import__
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
- def _import_hook(name, *args, **kwargs):
558
- result = original_import(name, *args, **kwargs)
559
-
560
- if name == package_module_name or name.startswith(f"{package_module_name}."):
561
- if package_module_name in sys.modules:
562
- import threading
563
- def _install_hook():
564
- if hasattr(__builtins__, '__import__'):
565
- __builtins__.__import__ = original_import
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
- if hasattr(__builtins__, '__import__'):
576
- __builtins__.__import__ = _import_hook
577
- else:
578
- import builtins
579
- builtins.__import__ = _import_hook
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: Dict[str, LazyLoader] = {}
654
- self._load_times: Dict[str, float] = {}
655
- self._access_counts: Dict[str, int] = {}
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) -> Dict[str, Any]:
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: Dict[str, str] = {}
737
- self._loaded_modules: Dict[str, ModuleType] = {}
738
- self._access_counts: Dict[str, int] = {}
739
- self._load_times: Dict[str, float] = {}
740
- self._background_tasks: Dict[str, asyncio.Task] = {}
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) -> Dict[str, Any]:
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: Dict[str, Set[str]] = defaultdict(set)
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: Dict[str, Tuple[str, ...]] = {}
1910
+ _lazy_prefix_method_registry: dict[str, tuple[str, ...]] = {}
1337
1911
 
1338
1912
  # Package class hints
1339
- _package_class_hints: Dict[str, Tuple[str, ...]] = {}
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: Tuple[str, ...] = tuple(
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) -> Tuple[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: Tuple[str, ...]) -> None:
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 or partially initialized
2063
+ # Fast path 2: Module already loaded - wrap it if needed
1493
2064
  if fullname in sys.modules:
1494
- if fullname == 'msgpack':
1495
- logger.info(f"[HOOK] msgpack already in sys.modules, skipping")
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
- if fullname.startswith('_'):
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 partially initialized")
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
- if package_name and installer.is_package_installed(package_name):
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 fullname == 'msgpack':
1555
- logger.info(f"[HOOK] msgpack import in progress, skipping")
1556
- return None
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 fullname == 'msgpack':
1560
- logger.info(f"[HOOK] Global importing active, skipping msgpack")
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: Tuple[str, ...] = ()
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 and not installer.is_package_installed(package_name):
1653
- logger.debug(f"[HOOK] Parent package {parent_package} not installed, intercepting parent")
1654
- return self.find_spec(parent_package, path, target)
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
- logger.debug(f"[HOOK] Failed to install/import {fullname}")
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
- if installer.is_async_enabled():
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.debug(f"Lazy import hook failed for {fullname}: {e}")
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: Set[str] = set()
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 _enhance_classes_with_class_methods(self, module):
1894
- """Enhance classes that registered lazy class methods."""
1895
- if module is None:
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
- methods_to_apply: Tuple[str, ...] = ()
1899
- for prefix, methods in _lazy_prefix_method_registry.items():
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
- if not methods_to_apply:
1905
- methods_to_apply = _DEFAULT_LAZY_METHODS
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
- if not methods_to_apply:
1908
- return
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
- if not isinstance(obj, type):
1913
- continue
1914
- for method_name in methods_to_apply:
1915
- attr = obj.__dict__.get(method_name)
1916
- if attr is None:
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 getattr(attr, "__lazy_wrapped__", False):
2612
+ # Skip if it's a wrapper class
2613
+ if obj.__name__.startswith('Lazy'):
1919
2614
  continue
1920
- if not callable(attr):
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
- if isinstance(attr, (classmethod, staticmethod)):
1924
- continue
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
- import inspect
1927
- try:
1928
- sig = inspect.signature(attr)
1929
- params = list(sig.parameters.keys())
1930
- if params and params[0] == 'self':
1931
- logger.debug(
1932
- "[LAZY ENHANCE] Wrapping instance method %s.%s.%s for class-level access",
1933
- module.__name__,
1934
- name,
1935
- method_name,
1936
- )
1937
- except Exception:
1938
- pass
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
- try:
1941
- original_func = attr
1942
-
1943
- def class_method_wrapper(func):
1944
- def _class_call(cls, *args, **kwargs):
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 enhanced:
1974
- log_event("enhance", logger.info, "[LAZY ENHANCE] Added %s convenience methods in %s", enhanced, module.__name__)
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: Dict[str, LazyMetaPathFinder] = {}
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