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
exonware/xwlazy/defs.py DELETED
@@ -1,378 +0,0 @@
1
- """
2
- #exonware/xwlazy/src/exonware/xwlazy/defs.py
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
- Generation Date: 10-Oct-2025
8
-
9
- Type Definitions and Constants for Lazy Loading System
10
-
11
- This module defines type definitions, constants, and TypedDict structures
12
- for the lazy loading system following GUIDE_ARCH.md structure.
13
- """
14
-
15
- from enum import Enum
16
- from typing import TypedDict, Dict, List, Optional, Any, Tuple
17
- from dataclasses import dataclass, field
18
- from types import ModuleType
19
-
20
- # =============================================================================
21
- # ENUMS
22
- # =============================================================================
23
-
24
- class LazyLoadMode(Enum):
25
- """Controls lazy module loading behavior."""
26
- NONE = "none" # Standard imports (no lazy loading)
27
- AUTO = "auto" # Lazy loading enabled (deferred module loading)
28
- PRELOAD = "preload" # Preload all modules on start
29
- BACKGROUND = "background" # Load modules in background threads
30
- CACHED = "cached" # Cache loaded modules but allow unloading
31
- # Superior performance modes
32
- TURBO = "turbo" # Multi-tier cache + parallel preloading + bytecode caching
33
- ADAPTIVE = "adaptive" # Self-optimizing with pattern learning
34
- HYPERPARALLEL = "hyperparallel" # Maximum parallelism with multi-threading
35
- STREAMING = "streaming" # Asynchronous background loading with streaming
36
- ULTRA = "ultra" # Aggressive optimizations (pre-compiled bytecode, mmap, etc.)
37
- INTELLIGENT = "intelligent" # Automatically switches to fastest mode based on load level
38
-
39
- class LazyInstallMode(Enum):
40
- """Lazy installation modes."""
41
- # Core modes
42
- NONE = "none" # No auto-installation
43
- SMART = "smart" # Install on first usage (on-demand) - replaces AUTO
44
- FULL = "full" # Install all dependencies on start
45
- CLEAN = "clean" # Install on usage + uninstall after completion
46
- TEMPORARY = "temporary" # Always uninstall after use (more aggressive than CLEAN)
47
- SIZE_AWARE = "size_aware" # Install small packages, skip large ones
48
-
49
- # Special purpose modes (kept for specific use cases)
50
- INTERACTIVE = "interactive" # Ask user before installing
51
- WARN = "warn" # Log warning but don't install (for monitoring)
52
- DISABLED = "disabled" # Don't install anything (alias for NONE, more explicit)
53
- DRY_RUN = "dry_run" # Show what would be installed but don't install
54
-
55
- class PathType(Enum):
56
- """Path types for validation."""
57
- FILE = "file"
58
- DIRECTORY = "directory"
59
- UNKNOWN = "unknown"
60
-
61
- class InstallStatus(Enum):
62
- """Installation status."""
63
- PENDING = "pending"
64
- RUNNING = "running"
65
- SUCCESS = "success"
66
- FAILED = "failed"
67
- SKIPPED = "skipped"
68
-
69
- class LoadLevel(Enum):
70
- """Load level categories."""
71
- LIGHT = "light_load"
72
- MEDIUM = "medium_load"
73
- HEAVY = "heavy_load"
74
- ENTERPRISE = "enterprise_load"
75
-
76
- # =============================================================================
77
- # DATACLASSES
78
- # =============================================================================
79
-
80
- @dataclass
81
- class DependencyInfo:
82
- """Information about a discovered dependency."""
83
- import_name: str
84
- package_name: str
85
- version: Optional[str] = None
86
- source: str = "unknown"
87
- category: str = "general"
88
-
89
- @dataclass
90
- class LazyModeConfig:
91
- """Two-dimensional lazy mode configuration combining load and install modes."""
92
- load_mode: LazyLoadMode = LazyLoadMode.NONE
93
- install_mode: LazyInstallMode = LazyInstallMode.NONE
94
-
95
- # Additional configuration options
96
- auto_uninstall_large: bool = False # For AUTO_MODE behavior
97
- large_package_threshold_mb: float = 50.0 # Size threshold for SIZE_AWARE mode
98
- preload_priority: List[str] = field(default_factory=list) # Priority modules for PRELOAD
99
- background_workers: int = 2 # Workers for BACKGROUND mode
100
-
101
- def __post_init__(self):
102
- """Normalize enum values."""
103
- if isinstance(self.load_mode, str):
104
- self.load_mode = LazyLoadMode(self.load_mode)
105
- if isinstance(self.install_mode, str):
106
- self.install_mode = LazyInstallMode(self.install_mode)
107
-
108
- @dataclass
109
- class InstallResult:
110
- """Result of an installation operation."""
111
- package_name: str
112
- success: bool
113
- status: InstallStatus
114
- error: Optional[str] = None
115
- version: Optional[str] = None
116
- source: Optional[str] = None # "cache", "pip", "wheel", etc.
117
-
118
- @dataclass
119
- class LazyConfig:
120
- """Bridge configuration settings with the lazy package implementation."""
121
- packages: Tuple[str, ...] = field(
122
- default_factory=lambda: ("default",)
123
- )
124
-
125
- def __post_init__(self) -> None:
126
- self.packages = tuple(package.lower() for package in self.packages)
127
-
128
- @dataclass(frozen=True)
129
- class PackageManifest:
130
- """Resolved manifest data for a single package."""
131
- package: str
132
- dependencies: Dict[str, str] = field(default_factory=dict)
133
- watched_prefixes: Tuple[str, ...] = ()
134
- async_installs: bool = False
135
- async_workers: int = 1
136
- class_wrap_prefixes: Tuple[str, ...] = ()
137
- metadata: Dict[str, Any] = field(default_factory=dict)
138
-
139
- def get_dependency(self, import_name: str) -> Optional[str]:
140
- """Return the declared package for the given import name."""
141
- if not import_name:
142
- return None
143
- direct = self.dependencies.get(import_name)
144
- if direct is not None:
145
- return direct
146
- # Case-insensitive fallback for convenience
147
- return self.dependencies.get(import_name.lower())
148
-
149
- @dataclass(frozen=True)
150
- class PackageData:
151
- """
152
- Immutable package data - same across all strategies.
153
-
154
- This data structure is used by all package caching, helper, and manager strategies.
155
- """
156
- name: str
157
- version: Optional[str] = None
158
- installed: bool = False
159
- install_time: Optional[float] = None
160
- access_count: int = 0
161
- install_mode: Optional['LazyInstallMode'] = None
162
- error: Optional[Exception] = None
163
- metadata: Dict[str, Any] = field(default_factory=dict)
164
-
165
- @dataclass(frozen=True)
166
- class ModuleData:
167
- """
168
- Immutable module data - same across all strategies.
169
-
170
- This data structure is used by all module caching, helper, and manager strategies.
171
- """
172
- path: str
173
- loaded_module: Optional['ModuleType'] = None
174
- loading: bool = False
175
- load_time: Optional[float] = None
176
- access_count: int = 0
177
- error: Optional[Exception] = None
178
- metadata: Dict[str, Any] = field(default_factory=dict)
179
-
180
- # =============================================================================
181
- # TYPE DEFINITIONS
182
- # =============================================================================
183
-
184
- class DependencyMapping(TypedDict, total=False):
185
- """Type definition for dependency mapping structure."""
186
- import_name: str
187
- package_name: str
188
- version: Optional[str]
189
- source: str
190
- category: str
191
-
192
- class PackageStats(TypedDict, total=False):
193
- """Type definition for package statistics."""
194
- enabled: bool
195
- mode: str
196
- package_name: str
197
- installed_packages: List[str]
198
- failed_packages: List[str]
199
- total_installed: int
200
- total_failed: int
201
-
202
- class LazyStatus(TypedDict, total=False):
203
- """Type definition for lazy mode status."""
204
- enabled: bool
205
- hook_installed: bool
206
- lazy_install_enabled: bool
207
- active: bool
208
- error: Optional[str]
209
-
210
- # =============================================================================
211
- # CONSTANTS
212
- # =============================================================================
213
-
214
- # Default configuration values
215
- DEFAULT_LARGE_PACKAGE_THRESHOLD_MB: float = 50.0
216
- DEFAULT_BACKGROUND_WORKERS: int = 2
217
- DEFAULT_PRELOAD_PRIORITY: List[str] = []
218
-
219
- # Common import -> package mappings (will be populated from discovery)
220
- COMMON_IMPORT_MAPPINGS: Dict[str, str] = {
221
- # Common mappings that are frequently used
222
- 'cv2': 'opencv-python',
223
- 'PIL': 'Pillow',
224
- 'yaml': 'PyYAML',
225
- 'toml': 'toml',
226
- 'pandas': 'pandas',
227
- 'numpy': 'numpy',
228
- 'scipy': 'scipy',
229
- 'sklearn': 'scikit-learn',
230
- 'matplotlib': 'matplotlib',
231
- 'seaborn': 'seaborn',
232
- 'requests': 'requests',
233
- 'urllib3': 'urllib3',
234
- 'bs4': 'beautifulsoup4',
235
- 'lxml': 'lxml',
236
- 'jinja2': 'Jinja2',
237
- 'flask': 'Flask',
238
- 'django': 'Django',
239
- 'fastapi': 'fastapi',
240
- 'pydantic': 'pydantic',
241
- 'sqlalchemy': 'SQLAlchemy',
242
- 'psycopg2': 'psycopg2-binary',
243
- 'pymongo': 'pymongo',
244
- 'redis': 'redis',
245
- 'celery': 'celery',
246
- 'boto3': 'boto3',
247
- 'azure': 'azure-storage-blob',
248
- 'google': 'google-cloud-storage',
249
- 'tensorflow': 'tensorflow',
250
- 'torch': 'torch',
251
- 'transformers': 'transformers',
252
- 'openai': 'openai',
253
- 'anthropic': 'anthropic',
254
- }
255
-
256
- # Package discovery source names
257
- DISCOVERY_SOURCE_PYPROJECT = "pyproject.toml"
258
- DISCOVERY_SOURCE_REQUIREMENTS = "requirements.txt"
259
- DISCOVERY_SOURCE_SETUP = "setup.py"
260
- DISCOVERY_SOURCE_POETRY = "poetry.lock"
261
- DISCOVERY_SOURCE_PIPFILE = "Pipfile"
262
- DISCOVERY_SOURCE_MANIFEST = "manifest.json"
263
-
264
- # Installation mode aliases
265
- INSTALL_MODE_ALIASES: Dict[str, str] = {
266
- 'auto': 'smart',
267
- 'on_demand': 'smart',
268
- 'on-demand': 'smart',
269
- 'lazy': 'smart',
270
- }
271
-
272
- # Cache keys
273
- CACHE_KEY_DEPENDENCIES = "dependencies"
274
- CACHE_KEY_PACKAGE_INFO = "package_info"
275
- CACHE_KEY_INSTALL_STATUS = "install_status"
276
- CACHE_KEY_DISCOVERY_SOURCES = "discovery_sources"
277
-
278
- # File patterns for discovery
279
- PYPROJECT_PATTERN = "pyproject.toml"
280
- REQUIREMENTS_PATTERN = "requirements*.txt"
281
- SETUP_PATTERN = "setup.py"
282
- POETRY_PATTERN = "poetry.lock"
283
- PIPFILE_PATTERN = "Pipfile"
284
-
285
- # =============================================================================
286
- # PRESET MODE MAPPINGS
287
- # =============================================================================
288
-
289
- # Preset mode combinations for convenience
290
- PRESET_MODES: Dict[str, LazyModeConfig] = {
291
- "none": LazyModeConfig(
292
- load_mode=LazyLoadMode.NONE,
293
- install_mode=LazyInstallMode.NONE
294
- ),
295
- "lite": LazyModeConfig(
296
- load_mode=LazyLoadMode.AUTO,
297
- install_mode=LazyInstallMode.NONE
298
- ),
299
- "smart": LazyModeConfig(
300
- load_mode=LazyLoadMode.AUTO,
301
- install_mode=LazyInstallMode.SMART
302
- ),
303
- "full": LazyModeConfig(
304
- load_mode=LazyLoadMode.AUTO,
305
- install_mode=LazyInstallMode.FULL
306
- ),
307
- "clean": LazyModeConfig(
308
- load_mode=LazyLoadMode.AUTO,
309
- install_mode=LazyInstallMode.CLEAN
310
- ),
311
- "temporary": LazyModeConfig(
312
- load_mode=LazyLoadMode.AUTO,
313
- install_mode=LazyInstallMode.TEMPORARY
314
- ),
315
- "size_aware": LazyModeConfig(
316
- load_mode=LazyLoadMode.AUTO,
317
- install_mode=LazyInstallMode.SIZE_AWARE
318
- ),
319
- "auto": LazyModeConfig(
320
- load_mode=LazyLoadMode.AUTO,
321
- install_mode=LazyInstallMode.SMART,
322
- auto_uninstall_large=True
323
- ),
324
- }
325
-
326
- def get_preset_mode(preset_name: str) -> Optional[LazyModeConfig]:
327
- """Get preset mode configuration by name."""
328
- return PRESET_MODES.get(preset_name.lower())
329
-
330
- # =============================================================================
331
- # EXPORT ALL
332
- # =============================================================================
333
-
334
- __all__ = [
335
- # Enums
336
- 'LazyLoadMode',
337
- 'LazyInstallMode',
338
- 'PathType',
339
- 'InstallStatus',
340
- 'LoadLevel',
341
- # Dataclasses
342
- 'DependencyInfo',
343
- 'LazyModeConfig',
344
- 'InstallResult',
345
- 'LazyConfig',
346
- 'PackageManifest',
347
- 'PackageData',
348
- 'ModuleData',
349
- # Type definitions
350
- 'DependencyMapping',
351
- 'PackageStats',
352
- 'LazyStatus',
353
- # Constants
354
- 'DEFAULT_LARGE_PACKAGE_THRESHOLD_MB',
355
- 'DEFAULT_BACKGROUND_WORKERS',
356
- 'DEFAULT_PRELOAD_PRIORITY',
357
- 'COMMON_IMPORT_MAPPINGS',
358
- 'DISCOVERY_SOURCE_PYPROJECT',
359
- 'DISCOVERY_SOURCE_REQUIREMENTS',
360
- 'DISCOVERY_SOURCE_SETUP',
361
- 'DISCOVERY_SOURCE_POETRY',
362
- 'DISCOVERY_SOURCE_PIPFILE',
363
- 'DISCOVERY_SOURCE_MANIFEST',
364
- 'INSTALL_MODE_ALIASES',
365
- 'CACHE_KEY_DEPENDENCIES',
366
- 'CACHE_KEY_PACKAGE_INFO',
367
- 'CACHE_KEY_INSTALL_STATUS',
368
- 'CACHE_KEY_DISCOVERY_SOURCES',
369
- 'PYPROJECT_PATTERN',
370
- 'REQUIREMENTS_PATTERN',
371
- 'SETUP_PATTERN',
372
- 'POETRY_PATTERN',
373
- 'PIPFILE_PATTERN',
374
- # Preset modes
375
- 'PRESET_MODES',
376
- 'get_preset_mode',
377
- ]
378
-
exonware/xwlazy/errors.py DELETED
@@ -1,276 +0,0 @@
1
- """
2
- #exonware/xwlazy/src/exonware/xwlazy/errors.py
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
-
8
- Generation Date: 10-Oct-2025
9
-
10
- Errors for Lazy Loading System
11
-
12
- This module defines all exception classes for the lazy loading system
13
- following GUIDE_ARCH.md structure.
14
- """
15
-
16
- from typing import Optional, Any
17
-
18
- # =============================================================================
19
- # BASE EXCEPTION
20
- # =============================================================================
21
-
22
- class LazySystemError(Exception):
23
- """
24
- Base exception for all lazy system errors.
25
-
26
- All lazy system exceptions inherit from this for easy error handling.
27
- """
28
-
29
- def __init__(self, message: str, package_name: Optional[str] = None):
30
- """
31
- Initialize lazy system error.
32
-
33
- Args:
34
- message: Error message
35
- package_name: Optional package name for scoped errors
36
- """
37
- self.package_name = package_name
38
- if package_name:
39
- message = f"[{package_name}] {message}"
40
- super().__init__(message)
41
-
42
- # =============================================================================
43
- # SPECIFIC EXCEPTIONS
44
- # =============================================================================
45
-
46
- class LazyInstallError(LazySystemError):
47
- """
48
- Raised when package installation fails.
49
-
50
- Examples:
51
- - pip install command fails
52
- - Package not found in PyPI
53
- - Network error during installation
54
- """
55
- pass
56
-
57
- class LazyDiscoveryError(LazySystemError):
58
- """
59
- Raised when dependency discovery fails.
60
-
61
- Examples:
62
- - Cannot read pyproject.toml
63
- - Invalid TOML syntax
64
- - Missing dependency configuration
65
- """
66
- pass
67
-
68
- class LazyHookError(LazySystemError):
69
- """
70
- Raised when import hook operation fails.
71
-
72
- Examples:
73
- - Cannot install hook in sys.meta_path
74
- - Hook is already installed
75
- - Hook interception fails
76
- """
77
- pass
78
-
79
- class LazySecurityError(LazySystemError):
80
- """
81
- Raised when security policy is violated.
82
-
83
- Examples:
84
- - Package not in allow list
85
- - Package in deny list
86
- - Untrusted package source
87
- """
88
- pass
89
-
90
- class ExternallyManagedError(LazyInstallError):
91
- """
92
- Raised when environment is externally managed (PEP 668).
93
-
94
- This happens when the Python environment has an EXTERNALLY-MANAGED
95
- marker file, preventing pip installations. Common in system Python
96
- installations on Linux distributions.
97
-
98
- Solutions:
99
- 1. Use a virtual environment
100
- 2. Use pipx for isolated installations
101
- 3. Override with --break-system-packages (not recommended)
102
- """
103
-
104
- def __init__(self, package_name: str):
105
- """
106
- Initialize externally managed error.
107
-
108
- Args:
109
- package_name: Package that cannot be installed
110
- """
111
- message = (
112
- f"Cannot install '{package_name}': Environment is externally managed (PEP 668). "
113
- f"Please use a virtual environment or pipx."
114
- )
115
- super().__init__(message, package_name=None)
116
-
117
- class DeferredImportError(Exception):
118
- """
119
- Placeholder for a failed import that will be retried when accessed.
120
-
121
- This enables two-stage lazy loading:
122
- - Stage 1: Import fails → Return DeferredImportError placeholder
123
- - Stage 2: On first use → Install missing package and replace with real module
124
-
125
- Performance optimized:
126
- - Zero overhead until user actually accesses the deferred import
127
- - Only installs dependencies when truly needed
128
- - Caches successful imports to avoid repeated installs
129
-
130
- Note: This is both an error class and a proxy object. It stays in
131
- errors.py because it represents an error state, but acts as
132
- a proxy until resolved.
133
- """
134
-
135
- __slots__ = (
136
- '_import_name',
137
- '_original_error',
138
- '_installer_package',
139
- '_retry_attempted',
140
- '_real_module',
141
- '_async_handle',
142
- )
143
-
144
- def __init__(
145
- self,
146
- import_name: str,
147
- original_error: Exception,
148
- installer_package: str,
149
- async_handle: Optional[Any] = None,
150
- ):
151
- """
152
- Initialize deferred import placeholder.
153
-
154
- Args:
155
- import_name: Name of the module that failed to import (e.g., 'fastavro')
156
- original_error: The original ImportError that was caught
157
- installer_package: Package name to use for lazy installation (e.g., 'xwsystem')
158
- """
159
- self._import_name = import_name
160
- self._original_error = original_error
161
- self._installer_package = installer_package
162
- self._retry_attempted = False
163
- self._real_module = None
164
- self._async_handle = async_handle
165
- super().__init__(f"Deferred import: {import_name}")
166
-
167
- def _try_install_and_import(self):
168
- """
169
- Attempt to install missing package and import it.
170
-
171
- Returns:
172
- The real module if installation succeeds
173
-
174
- Raises:
175
- Original ImportError if installation fails or is disabled
176
- """
177
- # Import from facade and new structure
178
- from .facade import lazy_import_with_install, is_lazy_install_enabled
179
- from .common.logger import get_logger
180
-
181
- logger = get_logger("xwlazy")
182
- logger.info(f"[STAGE 2] _try_install_and_import called for '{self._import_name}'")
183
-
184
- # Return cached module if already installed
185
- if self._real_module is not None:
186
- logger.info(f"[STAGE 2] Using cached module for '{self._import_name}'")
187
- return self._real_module
188
-
189
- # Only try once to avoid repeated failures
190
- if self._retry_attempted:
191
- logger.warning(f"[STAGE 2] Already attempted installation for '{self._import_name}', raising original error")
192
- raise self._original_error
193
-
194
- self._retry_attempted = True
195
-
196
- if self._async_handle is not None:
197
- logger.info(f"[STAGE 2] Waiting for async install of '{self._import_name}' to finish")
198
- self._async_handle.wait()
199
-
200
- # After async install, try simple import first (cache invalidation needed)
201
- try:
202
- import importlib
203
- import sys
204
- importlib.invalidate_caches()
205
- sys.path_importer_cache.clear()
206
- module = importlib.import_module(self._import_name)
207
- self._real_module = module
208
- logger.info(f"✅ [STAGE 2] Successfully loaded '{self._import_name}' after async install")
209
- return module
210
- except ImportError:
211
- pass # Fall through to lazy_import_with_install
212
-
213
- if not is_lazy_install_enabled(self._installer_package):
214
- logger.warning(f"[STAGE 2] Lazy install disabled for {self._installer_package}, cannot load {self._import_name}")
215
- raise self._original_error
216
-
217
- logger.info(f"⏳ [STAGE 2] Installing '{self._import_name}' on first use...")
218
-
219
- # Try to install and import
220
- module, success = lazy_import_with_install(
221
- self._import_name,
222
- installer_package=self._installer_package
223
- )
224
-
225
- if success and module:
226
- self._real_module = module
227
- logger.info(f"✅ [STAGE 2] Successfully installed and loaded '{self._import_name}'")
228
- return module
229
- else:
230
- logger.error(f"❌ [STAGE 2] Failed to install '{self._import_name}'")
231
- raise self._original_error
232
-
233
- def __call__(self, *args, **kwargs):
234
- """
235
- When user tries to instantiate, install dependency first.
236
-
237
- This enables: serializer = AvroSerializer() → installs fastavro → creates instance
238
- """
239
- module = self._try_install_and_import()
240
- # If module is callable (a class), instantiate it
241
- if callable(module):
242
- return module(*args, **kwargs)
243
- return module
244
-
245
- def __getattr__(self, name):
246
- """
247
- When user accesses attributes, install dependency first.
248
-
249
- This enables: from fastavro import reader → installs fastavro → returns reader
250
- """
251
- module = self._try_install_and_import()
252
- return getattr(module, name)
253
-
254
- def __repr__(self):
255
- """Show helpful message about deferred import."""
256
- if self._real_module is not None:
257
- return f"<DeferredImport: {self._import_name} (loaded)>"
258
- return f"<DeferredImport: {self._import_name} (will install on first use)>"
259
-
260
- # =============================================================================
261
- # EXPORT ALL
262
- # =============================================================================
263
-
264
- __all__ = [
265
- # Base exception
266
- 'LazySystemError',
267
- # Specific exceptions
268
- 'LazyInstallError',
269
- 'LazyDiscoveryError',
270
- 'LazyHookError',
271
- 'LazySecurityError',
272
- 'ExternallyManagedError',
273
- # Two-stage loading
274
- 'DeferredImportError',
275
- ]
276
-