exonware-xwlazy 0.1.0.22__py3-none-any.whl → 1.0.1.2__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 (90) hide show
  1. exonware/__init__.py +86 -16
  2. exonware/xwlazy/version.py +5 -5
  3. exonware/xwlazy.py +2546 -0
  4. exonware/xwlazy_external_libs.toml +716 -0
  5. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +6 -6
  6. exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
  7. exonware/xwlazy/__init__.py +0 -367
  8. exonware/xwlazy/common/__init__.py +0 -47
  9. exonware/xwlazy/common/base.py +0 -56
  10. exonware/xwlazy/common/cache.py +0 -504
  11. exonware/xwlazy/common/logger.py +0 -257
  12. exonware/xwlazy/common/services/__init__.py +0 -72
  13. exonware/xwlazy/common/services/dependency_mapper.py +0 -232
  14. exonware/xwlazy/common/services/install_async_utils.py +0 -165
  15. exonware/xwlazy/common/services/install_cache_utils.py +0 -245
  16. exonware/xwlazy/common/services/keyword_detection.py +0 -283
  17. exonware/xwlazy/common/services/spec_cache.py +0 -165
  18. exonware/xwlazy/common/services/state_manager.py +0 -84
  19. exonware/xwlazy/common/strategies/__init__.py +0 -28
  20. exonware/xwlazy/common/strategies/caching_dict.py +0 -44
  21. exonware/xwlazy/common/strategies/caching_installation.py +0 -88
  22. exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
  23. exonware/xwlazy/common/strategies/caching_lru.py +0 -63
  24. exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
  25. exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
  26. exonware/xwlazy/config.py +0 -193
  27. exonware/xwlazy/contracts.py +0 -1396
  28. exonware/xwlazy/defs.py +0 -378
  29. exonware/xwlazy/errors.py +0 -276
  30. exonware/xwlazy/facade.py +0 -991
  31. exonware/xwlazy/module/__init__.py +0 -18
  32. exonware/xwlazy/module/base.py +0 -565
  33. exonware/xwlazy/module/data.py +0 -17
  34. exonware/xwlazy/module/facade.py +0 -246
  35. exonware/xwlazy/module/importer_engine.py +0 -2117
  36. exonware/xwlazy/module/strategies/__init__.py +0 -22
  37. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  38. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  39. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  40. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  41. exonware/xwlazy/package/__init__.py +0 -18
  42. exonware/xwlazy/package/base.py +0 -798
  43. exonware/xwlazy/package/conf.py +0 -324
  44. exonware/xwlazy/package/data.py +0 -17
  45. exonware/xwlazy/package/facade.py +0 -480
  46. exonware/xwlazy/package/services/__init__.py +0 -84
  47. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  48. exonware/xwlazy/package/services/config_manager.py +0 -245
  49. exonware/xwlazy/package/services/discovery.py +0 -370
  50. exonware/xwlazy/package/services/host_packages.py +0 -145
  51. exonware/xwlazy/package/services/install_async.py +0 -277
  52. exonware/xwlazy/package/services/install_cache.py +0 -145
  53. exonware/xwlazy/package/services/install_interactive.py +0 -59
  54. exonware/xwlazy/package/services/install_policy.py +0 -156
  55. exonware/xwlazy/package/services/install_registry.py +0 -54
  56. exonware/xwlazy/package/services/install_result.py +0 -17
  57. exonware/xwlazy/package/services/install_sbom.py +0 -153
  58. exonware/xwlazy/package/services/install_utils.py +0 -79
  59. exonware/xwlazy/package/services/installer_engine.py +0 -406
  60. exonware/xwlazy/package/services/lazy_installer.py +0 -718
  61. exonware/xwlazy/package/services/manifest.py +0 -496
  62. exonware/xwlazy/package/services/strategy_registry.py +0 -186
  63. exonware/xwlazy/package/strategies/__init__.py +0 -57
  64. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  65. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  66. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  67. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  68. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  69. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  70. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  71. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  72. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  73. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  74. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  75. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  76. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  77. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  78. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  79. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  80. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  81. exonware/xwlazy/runtime/__init__.py +0 -18
  82. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  83. exonware/xwlazy/runtime/base.py +0 -274
  84. exonware/xwlazy/runtime/facade.py +0 -94
  85. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  86. exonware/xwlazy/runtime/metrics.py +0 -60
  87. exonware/xwlazy/runtime/performance.py +0 -37
  88. exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
  89. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
  90. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,718 +0,0 @@
1
- """
2
- Lazy Installer
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
-
8
- Generation Date: 15-Nov-2025
9
-
10
- Lazy installer that automatically installs missing packages on import failure.
11
- Each instance is isolated per package to prevent interference.
12
- """
13
-
14
- import os
15
- import sys
16
- import time
17
- import asyncio
18
- import threading
19
- import subprocess
20
- import importlib
21
- import importlib.util
22
- from pathlib import Path
23
- from typing import Dict, List, Optional, Tuple, Set, Any
24
- from collections import OrderedDict
25
- from types import ModuleType
26
-
27
- from ..base import APackageHelper
28
- from .config_manager import LazyInstallConfig
29
- from .manifest import PackageManifest
30
- from ...defs import LazyInstallMode
31
- from ...common.cache import InstallationCache
32
- from .install_policy import LazyInstallPolicy
33
- from .install_utils import (
34
- get_trigger_file,
35
- is_externally_managed,
36
- check_pip_audit_available
37
- )
38
- from .async_install_handle import AsyncInstallHandle
39
- from .install_interactive import InteractiveInstallMixin
40
- from .install_cache import InstallCacheMixin
41
- from .install_async import AsyncInstallMixin
42
- from .install_sbom import SBOMAuditMixin
43
-
44
- # Lazy import for DependencyMapper to avoid circular dependency
45
- def _get_dependency_mapper():
46
- """Get DependencyMapper (lazy import to avoid circular dependency)."""
47
- from ...common.services.dependency_mapper import DependencyMapper
48
- return DependencyMapper
49
-
50
- DependencyMapper = None # Will be initialized on first use
51
-
52
- # Lazy imports to avoid circular dependency
53
- def _get_logger():
54
- """Get logger (lazy import to avoid circular dependency)."""
55
- from ...common.logger import get_logger
56
- return get_logger("xwlazy.lazy_installer")
57
-
58
- def _get_log_event():
59
- """Get log_event function (lazy import to avoid circular dependency)."""
60
- from ...common.logger import log_event
61
- return log_event
62
-
63
- def _get_print_formatted():
64
- """Get print_formatted function (lazy import to avoid circular dependency)."""
65
- from ...common.logger import print_formatted
66
- return print_formatted
67
-
68
- def _get_installing_state():
69
- """Get installing state (lazy import to avoid circular dependency)."""
70
- from ...module.importer_engine import get_installing_state
71
- return get_installing_state()
72
-
73
- def _get_spec_cache_put():
74
- """Get spec_cache_put function (lazy import to avoid circular dependency)."""
75
- from ...common.services.spec_cache import _spec_cache_put
76
- return _spec_cache_put
77
-
78
- def _get_spec_cache_clear():
79
- """Get spec_cache_clear function (lazy import to avoid circular dependency)."""
80
- from ...common.services.spec_cache import _spec_cache_clear
81
- return _spec_cache_clear
82
-
83
- logger = None # Will be initialized on first use
84
- _log = None # Will be initialized on first use
85
- print_formatted = None # Will be initialized on first use
86
- _installing = None # Will be initialized on first use
87
- _spec_cache_put = None # Will be initialized on first use
88
- _spec_cache_clear = None # Will be initialized on first use
89
-
90
- # Environment variables
91
- _ENV_ASYNC_INSTALL = os.environ.get("XWLAZY_ASYNC_INSTALL", "").strip().lower() in {"1", "true", "yes", "on"}
92
- _ENV_ASYNC_WORKERS = int(os.environ.get("XWLAZY_ASYNC_WORKERS", "0") or 0)
93
- _KNOWN_MISSING_CACHE_LIMIT = int(os.environ.get("XWLAZY_MISSING_CACHE_MAX", "128") or 128)
94
- _KNOWN_MISSING_CACHE_TTL = float(os.environ.get("XWLAZY_MISSING_CACHE_TTL", "120") or 120.0)
95
- _DEFAULT_ASYNC_CACHE_DIR = Path(
96
- os.environ.get(
97
- "XWLAZY_ASYNC_CACHE_DIR",
98
- os.path.join(os.path.expanduser("~"), ".xwlazy", "wheel-cache"),
99
- )
100
- )
101
-
102
- def _ensure_logging_initialized():
103
- """Ensure logging utilities are initialized (lazy init to avoid circular imports)."""
104
- global logger, _log, print_formatted, _installing, _spec_cache_put, _spec_cache_clear
105
- if logger is None:
106
- logger = _get_logger()
107
- if _log is None:
108
- _log = _get_log_event()
109
- if print_formatted is None:
110
- print_formatted = _get_print_formatted()
111
- if _installing is None:
112
- _installing = _get_installing_state()
113
- if _spec_cache_put is None:
114
- _spec_cache_put = _get_spec_cache_put()
115
- if _spec_cache_clear is None:
116
- _spec_cache_clear = _get_spec_cache_clear()
117
-
118
- class LazyInstaller(
119
- APackageHelper,
120
- InteractiveInstallMixin,
121
- InstallCacheMixin,
122
- AsyncInstallMixin,
123
- SBOMAuditMixin
124
- ):
125
- """
126
- Lazy installer that automatically installs missing packages on import failure.
127
- Each instance is isolated per package to prevent interference.
128
-
129
- This class extends APackageHelper and provides comprehensive installation functionality.
130
- """
131
-
132
- __slots__ = APackageHelper.__slots__ + (
133
- '_dependency_mapper',
134
- '_auto_approve_all',
135
- '_async_enabled',
136
- '_async_workers',
137
- '_async_loop',
138
- '_async_tasks',
139
- '_known_missing',
140
- '_async_cache_dir',
141
- '_loop_thread',
142
- '_install_cache',
143
- )
144
-
145
- def __init__(self, package_name: str = 'default'):
146
- """Initialize lazy installer for a specific package."""
147
- super().__init__(package_name)
148
- # Lazy init to avoid circular dependency
149
- global DependencyMapper
150
- if DependencyMapper is None:
151
- DependencyMapper = _get_dependency_mapper()
152
- self._dependency_mapper = DependencyMapper(package_name)
153
- self._auto_approve_all = False
154
- self._async_enabled = False
155
- self._async_workers = 1
156
- self._async_loop: Optional[asyncio.AbstractEventLoop] = None
157
- self._async_tasks: Dict[str, Any] = {}
158
- self._known_missing: OrderedDict[str, float] = OrderedDict()
159
- self._async_cache_dir = _DEFAULT_ASYNC_CACHE_DIR
160
- self._loop_thread: Optional[threading.Thread] = None
161
-
162
- # ROOT CAUSE FIX: Load persistent installation cache
163
- # This cache tracks installed packages across Python restarts
164
- # and prevents unnecessary importability checks and installations
165
- self._install_cache = InstallationCache()
166
-
167
- def install_package(self, package_name: str, module_name: str = None) -> bool:
168
- """Install a package using pip."""
169
- _ensure_logging_initialized()
170
- # CRITICAL: Set flag FIRST before ANY operations to prevent recursion
171
- if getattr(_installing, 'active', False):
172
- print(f"[DEBUG] Installation already in progress, skipping {package_name} to prevent recursion")
173
- return False
174
-
175
- # Check global recursion depth to prevent infinite recursion
176
- from ...module.importer_engine import _installation_depth, _installation_depth_lock
177
- with _installation_depth_lock:
178
- if _installation_depth > 0:
179
- print(f"[DEBUG] Installation recursion detected (depth={_installation_depth}), skipping {package_name}")
180
- return False
181
- _installation_depth += 1
182
-
183
- # Set flag IMMEDIATELY to prevent any imports during installation from triggering recursion
184
- _installing.active = True
185
-
186
- try:
187
- with self._lock:
188
- if package_name in self._installed_packages:
189
- return True
190
-
191
- if package_name in self._failed_packages:
192
- return False
193
-
194
- if self._mode == LazyInstallMode.DISABLED or self._mode == LazyInstallMode.NONE:
195
- _log("install", f"Lazy installation disabled for {self._package_name}, skipping {package_name}")
196
- return False
197
-
198
- if self._mode == LazyInstallMode.WARN:
199
- logger.warning(
200
- f"[WARN] Package '{package_name}' is missing but WARN mode is active - not installing"
201
- )
202
- print(
203
- f"[WARN] ({self._package_name}): Package '{package_name}' is missing "
204
- f"(not installed in WARN mode)"
205
- )
206
- return False
207
-
208
- if self._mode == LazyInstallMode.DRY_RUN:
209
- print(f"[DRY RUN] ({self._package_name}): Would install package '{package_name}'")
210
- return False
211
-
212
- if self._mode == LazyInstallMode.INTERACTIVE:
213
- if not self._ask_user_permission(package_name, module_name or package_name):
214
- _log("install", f"User declined installation of {package_name}")
215
- self._failed_packages.add(package_name)
216
- return False
217
-
218
- # Security checks
219
- if is_externally_managed():
220
- logger.error(f"Cannot install {package_name}: Environment is externally managed (PEP 668)")
221
- print(f"\n[ERROR] This Python environment is externally managed (PEP 668)")
222
- print(f"Package '{package_name}' cannot be installed in this environment.")
223
- print(f"\nSuggested solutions:")
224
- print(f" 1. Create a virtual environment:")
225
- print(f" python -m venv .venv")
226
- print(f" .venv\\Scripts\\activate # Windows")
227
- print(f" source .venv/bin/activate # Linux/macOS")
228
- print(f" 2. Use pipx for isolated installs:")
229
- print(f" pipx install {package_name}")
230
- print(f" 3. Override with --break-system-packages (NOT RECOMMENDED)\n")
231
- self._failed_packages.add(package_name)
232
- return False
233
-
234
- allowed, reason = LazyInstallPolicy.is_package_allowed(self._package_name, package_name)
235
- if not allowed:
236
- logger.error(f"Cannot install {package_name}: {reason}")
237
- print(f"\n[SECURITY] Package '{package_name}' blocked: {reason}\n")
238
- self._failed_packages.add(package_name)
239
- return False
240
-
241
- # Show warning about missing library with trigger file
242
- trigger_file = get_trigger_file()
243
- module_display = module_name or package_name
244
- if trigger_file:
245
- used_for = module_display if module_display != package_name else package_name
246
- print_formatted(
247
- "WARN",
248
- f"Missing library {package_name} used for ({used_for}) triggered by {trigger_file}",
249
- same_line=True
250
- )
251
- else:
252
- print_formatted(
253
- "WARN",
254
- f"Missing library {package_name} used for ({module_display})",
255
- same_line=True
256
- )
257
-
258
- # Proceed with installation
259
- try:
260
- print_formatted("INFO", f"Installing package: {package_name}", same_line=True)
261
- policy_args = LazyInstallPolicy.get_pip_args(self._package_name) or []
262
-
263
- cache_args = list(policy_args)
264
- if self._install_from_cached_tree(package_name):
265
- print_formatted("ACTION", f"Installing {package_name} via pip...", same_line=True)
266
- time.sleep(0.1)
267
- self._finalize_install_success(package_name, "cache-tree")
268
- return True
269
-
270
- if self._install_from_cached_wheel(package_name, cache_args):
271
- print_formatted("ACTION", f"Installing {package_name} via pip...", same_line=True)
272
- wheel_path = self._cached_wheel_name(package_name)
273
- self._materialize_cached_tree(package_name, wheel_path)
274
- time.sleep(0.1)
275
- self._finalize_install_success(package_name, "cache")
276
- return True
277
-
278
- wheel_path = self._ensure_cached_wheel(package_name, cache_args)
279
- if wheel_path and self._pip_install_from_path(wheel_path, cache_args):
280
- print_formatted("ACTION", f"Installing {package_name} via pip...", same_line=True)
281
- self._materialize_cached_tree(package_name, wheel_path)
282
- time.sleep(0.1)
283
- self._finalize_install_success(package_name, "wheel")
284
- return True
285
-
286
- # Show installation message with animated dots
287
- print_formatted("ACTION", f"Installing {package_name} via pip...", same_line=True)
288
-
289
- # Animate dots while installing
290
- stop_animation = threading.Event()
291
-
292
- def animate_dots():
293
- dots = ["", ".", "..", "..."]
294
- i = 0
295
- while not stop_animation.is_set():
296
- msg = f"Installing {package_name} via pip{dots[i % len(dots)]}"
297
- print_formatted("ACTION", msg, same_line=True)
298
- i += 1
299
- time.sleep(0.3)
300
-
301
- animator = threading.Thread(target=animate_dots, daemon=True)
302
- animator.start()
303
-
304
- try:
305
- pip_args = [sys.executable, '-m', 'pip', 'install']
306
- if policy_args:
307
- pip_args.extend(policy_args)
308
- logger.debug(f"Using policy args: {policy_args}")
309
-
310
- pip_args.append(package_name)
311
-
312
- result = subprocess.run(
313
- pip_args,
314
- capture_output=True,
315
- text=True,
316
- check=True
317
- )
318
- finally:
319
- stop_animation.set()
320
- animator.join(timeout=0.5)
321
-
322
- self._finalize_install_success(package_name, "pip")
323
- wheel_path = self._ensure_cached_wheel(package_name, cache_args)
324
- if wheel_path:
325
- self._materialize_cached_tree(package_name, wheel_path)
326
- return True
327
-
328
- except subprocess.CalledProcessError as e:
329
- logger.error(f"Failed to install {package_name}: {e.stderr}")
330
- print(f"[FAIL] Failed to install {package_name}\n")
331
- self._failed_packages.add(package_name)
332
- return False
333
- except Exception as e:
334
- logger.error(f"Unexpected error installing {package_name}: {e}")
335
- print(f"[ERROR] Unexpected error: {e}\n")
336
- self._failed_packages.add(package_name)
337
- return False
338
- finally:
339
- # CRITICAL: Always clear the installing flag
340
- _installing.active = False
341
- # Decrement global recursion depth
342
- from ...module.importer_engine import _installation_depth, _installation_depth_lock
343
- with _installation_depth_lock:
344
- _installation_depth = max(0, _installation_depth - 1)
345
-
346
- def _finalize_install_success(self, package_name: str, source: str) -> None:
347
- """Finalize successful installation by updating caches."""
348
- # Update in-memory cache
349
- self._installed_packages.add(package_name)
350
-
351
- # ROOT CAUSE FIX: Mark in persistent cache (survives Python restarts)
352
- version = self._get_installed_version(package_name)
353
- self._install_cache.mark_installed(package_name, version)
354
-
355
- print_formatted("SUCCESS", f"Successfully installed via {source}: {package_name}", same_line=True)
356
- print()
357
-
358
- # CRITICAL: Invalidate import caches so Python can see newly installed modules
359
- importlib.invalidate_caches()
360
- sys.path_importer_cache.clear()
361
-
362
- if check_pip_audit_available():
363
- self._run_vulnerability_audit(package_name)
364
- self._update_lockfile(package_name)
365
-
366
- def _get_installed_version(self, package_name: str) -> Optional[str]:
367
- """Get installed version of a package."""
368
- try:
369
- result = subprocess.run(
370
- [sys.executable, '-m', 'pip', 'show', package_name],
371
- capture_output=True,
372
- text=True,
373
- timeout=5
374
- )
375
-
376
- if result.returncode == 0:
377
- for line in result.stdout.split('\n'):
378
- if line.startswith('Version:'):
379
- return line.split(':', 1)[1].strip()
380
- except Exception as e:
381
- logger.debug(f"Could not get version for {package_name}: {e}")
382
- return None
383
-
384
- def uninstall_package(self, package_name: str, quiet: bool = True) -> bool:
385
- """Uninstall a package (synchronous wrapper)."""
386
- if self._async_loop and self._async_loop.is_running():
387
- asyncio.run_coroutine_threadsafe(
388
- self.uninstall_package_async(package_name, quiet=quiet),
389
- self._async_loop
390
- )
391
- return True
392
- else:
393
- try:
394
- result = subprocess.run(
395
- [sys.executable, '-m', 'pip', 'uninstall', '-y', package_name],
396
- capture_output=quiet,
397
- check=False
398
- )
399
- if result.returncode == 0:
400
- with self._lock:
401
- self._installed_packages.discard(package_name)
402
- return True
403
- return False
404
- except Exception as e:
405
- logger.debug(f"Failed to uninstall {package_name}: {e}")
406
- return False
407
-
408
- def uninstall_all_packages(self, quiet: bool = True) -> None:
409
- """Uninstall all packages installed by this installer."""
410
- with self._lock:
411
- packages_to_uninstall = list(self._installed_packages)
412
- for package_name in packages_to_uninstall:
413
- self.uninstall_package(package_name, quiet=quiet)
414
-
415
- def _is_module_importable(self, module_name: str) -> bool:
416
- """Check if module can be imported without installation."""
417
- try:
418
- spec = importlib.util.find_spec(module_name)
419
- return spec is not None and spec.loader is not None
420
- except (ValueError, AttributeError, ImportError, Exception):
421
- return False
422
-
423
- def is_package_installed(self, package_name: str) -> bool:
424
- """Check if a package is already installed."""
425
- # Step 1: Check persistent cache FIRST (fastest, no importability check needed)
426
- if self._install_cache.is_installed(package_name):
427
- # Also update in-memory cache for performance
428
- self._installed_packages.add(package_name)
429
- return True
430
-
431
- # Step 2: Check in-memory cache (fast, but lost on restart)
432
- if package_name in self._installed_packages:
433
- return True
434
-
435
- # Step 3: Check actual importability (slower, but accurate)
436
- try:
437
- # Get module name from package name (heuristic)
438
- module_name = package_name.replace('-', '_')
439
- if self._is_module_importable(module_name):
440
- # Cache in both persistent and in-memory cache
441
- version = self._get_installed_version(package_name)
442
- self._install_cache.mark_installed(package_name, version)
443
- self._installed_packages.add(package_name)
444
- return True
445
- except (ImportError, AttributeError, ValueError) as e:
446
- # Expected errors when checking package installation
447
- _ensure_logging_initialized()
448
- if logger:
449
- logger.debug(f"Package check failed for {package_name}: {e}")
450
- pass
451
- except Exception as e:
452
- # Unexpected errors - log but don't fail
453
- _ensure_logging_initialized()
454
- if logger:
455
- logger.debug(f"Unexpected error checking package {package_name}: {e}")
456
- pass
457
-
458
- return False
459
-
460
- def install_and_import(self, module_name: str, package_name: str = None) -> Tuple[Optional[ModuleType], bool]:
461
- """
462
- Install package and import module.
463
-
464
- ROOT CAUSE FIX: Check if module is importable FIRST before attempting
465
- installation. This prevents circular imports and unnecessary installations.
466
- """
467
- # CRITICAL: Prevent recursion - if installation is already in progress, skip
468
- if getattr(_installing, 'active', False):
469
- logger.debug(
470
- f"Installation in progress, skipping install_and_import for {module_name} "
471
- f"to prevent recursion"
472
- )
473
- return None, False
474
-
475
- if not self.is_enabled():
476
- return None, False
477
-
478
- # Get package name early for cache check
479
- if package_name is None:
480
- package_name = self._dependency_mapper.get_package_name(module_name)
481
-
482
- # ROOT CAUSE FIX: Check persistent cache FIRST (fastest, no importability check)
483
- if package_name and self._install_cache.is_installed(package_name):
484
- # Package is in persistent cache - import directly
485
- try:
486
- module = importlib.import_module(module_name)
487
- self._clear_module_missing(module_name)
488
- _spec_cache_put(module_name, importlib.util.find_spec(module_name))
489
- logger.debug(f"Module {module_name} is in persistent cache, imported directly")
490
- return module, True
491
- except ImportError as e:
492
- logger.debug(f"Module {module_name} in cache but import failed: {e}")
493
- # Cache might be stale - fall through to importability check
494
-
495
- # ROOT CAUSE FIX: Check if module is ALREADY importable BEFORE doing anything else
496
- if self._is_module_importable(module_name):
497
- # Module is already importable - import it directly
498
- if package_name:
499
- version = self._get_installed_version(package_name)
500
- self._install_cache.mark_installed(package_name, version)
501
- try:
502
- module = importlib.import_module(module_name)
503
- self._clear_module_missing(module_name)
504
- _spec_cache_put(module_name, importlib.util.find_spec(module_name))
505
- logger.debug(f"Module {module_name} is already importable, imported directly")
506
- return module, True
507
- except ImportError as e:
508
- logger.debug(f"Module {module_name} appeared importable but import failed: {e}")
509
-
510
- # Package name should already be set from cache check above
511
- if package_name is None:
512
- package_name = self._dependency_mapper.get_package_name(module_name)
513
- if package_name is None:
514
- logger.debug(f"Module '{module_name}' is a system/built-in module, not installing")
515
- return None, False
516
-
517
- # Module is NOT importable - need to install it
518
- # ROOT CAUSE FIX: Temporarily remove ALL xwlazy finders from sys.meta_path
519
- xwlazy_finder_names = {'LazyMetaPathFinder', 'LazyPathFinder', 'LazyLoader'}
520
- xwlazy_finders = [f for f in sys.meta_path if type(f).__name__ in xwlazy_finder_names]
521
- for finder in xwlazy_finders:
522
- try:
523
- sys.meta_path.remove(finder)
524
- except ValueError:
525
- pass
526
-
527
- try:
528
- # Try importing again after removing finders (in case it was a false negative)
529
- module = importlib.import_module(module_name)
530
- self._clear_module_missing(module_name)
531
- _spec_cache_put(module_name, importlib.util.find_spec(module_name))
532
- return module, True
533
- except ImportError:
534
- pass
535
- finally:
536
- # Restore finders in reverse order to maintain original position
537
- for finder in reversed(xwlazy_finders):
538
- if finder not in sys.meta_path:
539
- sys.meta_path.insert(0, finder)
540
-
541
- if self._async_enabled:
542
- handle = self.schedule_async_install(module_name)
543
- if handle is not None:
544
- return None, False
545
-
546
- if self.install_package(package_name, module_name):
547
- for attempt in range(3):
548
- try:
549
- importlib.invalidate_caches()
550
- sys.path_importer_cache.clear()
551
-
552
- # ROOT CAUSE FIX: Remove ALL xwlazy finders before importing
553
- xwlazy_finder_names = {'LazyMetaPathFinder', 'LazyPathFinder', 'LazyLoader'}
554
- xwlazy_finders = [f for f in sys.meta_path if type(f).__name__ in xwlazy_finder_names]
555
- for finder in xwlazy_finders:
556
- try:
557
- sys.meta_path.remove(finder)
558
- except ValueError:
559
- pass
560
-
561
- try:
562
- module = importlib.import_module(module_name)
563
- self._clear_module_missing(module_name)
564
- _spec_cache_put(module_name, importlib.util.find_spec(module_name))
565
- # ROOT CAUSE FIX: Mark in both persistent and in-memory cache
566
- version = self._get_installed_version(package_name)
567
- self._install_cache.mark_installed(package_name, version)
568
- self._installed_packages.add(package_name)
569
- return module, True
570
- finally:
571
- # Restore finders in reverse order to maintain original position
572
- for finder in reversed(xwlazy_finders):
573
- if finder not in sys.meta_path:
574
- sys.meta_path.insert(0, finder)
575
- except ImportError as e:
576
- if attempt < 2:
577
- time.sleep(0.1 * (attempt + 1))
578
- else:
579
- logger.error(f"Still cannot import {module_name} after installing {package_name}: {e}")
580
- return None, False
581
-
582
- self._mark_module_missing(module_name)
583
- return None, False
584
-
585
- def _check_security_policy(self, package_name: str) -> Tuple[bool, str]:
586
- """Check security policy for package."""
587
- return LazyInstallPolicy.is_package_allowed(self._package_name, package_name)
588
-
589
- def _run_pip_install(self, package_name: str, args: List[str]) -> bool:
590
- """Run pip install with arguments."""
591
- if self._install_from_cached_wheel(package_name):
592
- return True
593
- try:
594
- pip_args = [
595
- sys.executable,
596
- '-m',
597
- 'pip',
598
- 'install',
599
- '--disable-pip-version-check',
600
- '--no-input',
601
- ] + args + [package_name]
602
- result = subprocess.run(
603
- pip_args,
604
- capture_output=True,
605
- text=True,
606
- check=True,
607
- )
608
- if result.returncode == 0:
609
- self._ensure_cached_wheel(package_name)
610
- return True
611
- return False
612
- except subprocess.CalledProcessError:
613
- return False
614
-
615
- def get_installed_packages(self) -> Set[str]:
616
- """Get set of installed package names."""
617
- with self._lock:
618
- return self._installed_packages.copy()
619
-
620
- def get_failed_packages(self) -> Set[str]:
621
- """Get set of failed package names."""
622
- with self._lock:
623
- return self._failed_packages.copy()
624
-
625
- def get_async_tasks(self) -> Dict[str, Any]:
626
- """Get dictionary of async installation tasks."""
627
- with self._lock:
628
- return {
629
- module_name: {
630
- 'done': task.done() if task else False,
631
- 'cancelled': task.cancelled() if task else False,
632
- }
633
- for module_name, task in self._async_tasks.items()
634
- }
635
-
636
- def get_stats(self) -> Dict[str, Any]:
637
- """Get installation statistics (extends base class method)."""
638
- base_stats = super().get_stats()
639
- with self._lock:
640
- base_stats.update({
641
- 'async_enabled': self._async_enabled,
642
- 'async_workers': self._async_workers,
643
- 'async_tasks_count': len(self._async_tasks),
644
- 'known_missing_count': len(self._known_missing),
645
- 'auto_approve_all': self._auto_approve_all,
646
- })
647
- return base_stats
648
-
649
- # Abstract method implementations from APackageHelper
650
- def _discover_from_sources(self) -> None:
651
- """Discover dependencies from all sources."""
652
- # Lazy import to avoid circular dependency
653
- from ...package.discovery import get_lazy_discovery
654
- discovery = get_lazy_discovery()
655
- if discovery:
656
- all_deps = discovery.discover_all_dependencies()
657
- for import_name, package_name in all_deps.items():
658
- from ...defs import DependencyInfo
659
- self.discovered_dependencies[import_name] = DependencyInfo(
660
- import_name=import_name,
661
- package_name=package_name,
662
- source='discovery',
663
- category='discovered'
664
- )
665
-
666
- def _is_cache_valid(self) -> bool:
667
- """Check if cached dependencies are still valid."""
668
- # Use discovery's cache validation
669
- # Lazy import to avoid circular dependency
670
- from ...package.discovery import get_lazy_discovery
671
- discovery = get_lazy_discovery()
672
- if discovery:
673
- return discovery._is_cache_valid()
674
- return False
675
-
676
- def _add_common_mappings(self) -> None:
677
- """Add common import -> package mappings."""
678
- # Common mappings are handled by discovery
679
- pass
680
-
681
- def _update_file_mtimes(self) -> None:
682
- """Update file modification times for cache validation."""
683
- # File mtimes are handled by discovery
684
- pass
685
-
686
- def _check_importability(self, package_name: str) -> bool:
687
- """Check if package is importable."""
688
- return self.is_package_installed(package_name)
689
-
690
- def _check_persistent_cache(self, package_name: str) -> bool:
691
- """Check persistent cache for package installation status."""
692
- return self._install_cache.is_installed(package_name)
693
-
694
- def _mark_installed_in_persistent_cache(self, package_name: str) -> None:
695
- """Mark package as installed in persistent cache."""
696
- version = self._get_installed_version(package_name)
697
- self._install_cache.mark_installed(package_name, version)
698
-
699
- def _mark_uninstalled_in_persistent_cache(self, package_name: str) -> None:
700
- """Mark package as uninstalled in persistent cache."""
701
- self._install_cache.mark_uninstalled(package_name)
702
-
703
- def _run_install(self, *package_names: str) -> None:
704
- """Run pip install for packages."""
705
- for package_name in package_names:
706
- self.install_package(package_name)
707
-
708
- def _run_uninstall(self, *package_names: str) -> None:
709
- """Run pip uninstall for packages."""
710
- for package_name in package_names:
711
- self.uninstall_package(package_name, quiet=True)
712
-
713
- def is_cache_valid(self, key: str) -> bool:
714
- """Check if cache entry is still valid."""
715
- return self._is_cache_valid()
716
-
717
- __all__ = ['LazyInstaller']
718
-