exonware-xwlazy 0.1.0.10__py3-none-any.whl → 0.1.0.11__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.
@@ -0,0 +1,465 @@
1
+ """
2
+ #exonware/xwsystem/src/exonware/xwsystem/utils/lazy_package/lazy_base.py
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+ Version: 0.1.0.16
8
+ Generation Date: 10-Oct-2025
9
+
10
+ Abstract Base Classes for Lazy Loading System
11
+
12
+ This module defines all abstract base classes for the lazy loading system
13
+ following DEV_GUIDELINES.md structure. All abstract classes start with 'A'
14
+ and extend interfaces from lazy_contracts.py.
15
+
16
+ Design Patterns:
17
+ - Template Method: Base classes define common workflows with abstract steps
18
+ - Strategy: Different implementations can be plugged in
19
+ - Abstract Factory: Factory methods for creating instances
20
+ """
21
+
22
+ import threading
23
+ from abc import ABC, abstractmethod
24
+ from pathlib import Path
25
+ from typing import Dict, List, Optional, Any, Set, Tuple
26
+ from types import ModuleType
27
+
28
+ from .lazy_contracts import (
29
+ IPackageDiscovery,
30
+ IPackageInstaller,
31
+ IImportHook,
32
+ IPackageCache,
33
+ ILazyLoader,
34
+ DependencyInfo,
35
+ LazyInstallMode,
36
+ )
37
+
38
+
39
+ # =============================================================================
40
+ # ABSTRACT DISCOVERY (Template Method Pattern)
41
+ # =============================================================================
42
+
43
+ class APackageDiscovery(IPackageDiscovery, ABC):
44
+ """
45
+ Abstract base for package discovery.
46
+
47
+ Implements Template Method pattern where discover_all_dependencies()
48
+ defines the overall workflow, and subclasses implement specific steps.
49
+ """
50
+
51
+ __slots__ = ('project_root', 'discovered_dependencies', '_discovery_sources',
52
+ '_cached_dependencies', '_file_mtimes', '_cache_valid')
53
+
54
+ def __init__(self, project_root: Optional[str] = None):
55
+ """
56
+ Initialize package discovery.
57
+
58
+ Args:
59
+ project_root: Root directory of project (auto-detected if None)
60
+ """
61
+ self.project_root = Path(project_root) if project_root else self._find_project_root()
62
+ self.discovered_dependencies: Dict[str, DependencyInfo] = {}
63
+ self._discovery_sources: List[str] = []
64
+ self._cached_dependencies: Dict[str, str] = {}
65
+ self._file_mtimes: Dict[str, float] = {}
66
+ self._cache_valid = False
67
+
68
+ def _find_project_root(self) -> Path:
69
+ """Find the project root directory by looking for markers."""
70
+ current = Path(__file__).parent
71
+ while current != current.parent:
72
+ if (current / 'pyproject.toml').exists() or (current / 'setup.py').exists():
73
+ return current
74
+ current = current.parent
75
+ return Path.cwd()
76
+
77
+ def discover_all_dependencies(self) -> Dict[str, str]:
78
+ """
79
+ Template method: Discover all dependencies from all sources.
80
+
81
+ Workflow:
82
+ 1. Check if cache is valid
83
+ 2. If not, discover from sources
84
+ 3. Add common mappings
85
+ 4. Update cache
86
+ 5. Return dependencies
87
+
88
+ Returns:
89
+ Dict mapping import_name -> package_name
90
+ """
91
+ # Return cached result if still valid
92
+ if self._is_cache_valid():
93
+ return self._cached_dependencies.copy()
94
+
95
+ # Cache invalid - rediscover
96
+ self.discovered_dependencies.clear()
97
+ self._discovery_sources.clear()
98
+
99
+ # Discover from all sources (abstract method)
100
+ self._discover_from_sources()
101
+
102
+ # Add common mappings
103
+ self._add_common_mappings()
104
+
105
+ # Convert to simple dict format and cache
106
+ result = {}
107
+ for import_name, dep_info in self.discovered_dependencies.items():
108
+ result[import_name] = dep_info.package_name
109
+
110
+ # Update cache
111
+ self._cached_dependencies = result.copy()
112
+ self._cache_valid = True
113
+ self._update_file_mtimes()
114
+
115
+ return result
116
+
117
+ @abstractmethod
118
+ def _discover_from_sources(self) -> None:
119
+ """
120
+ Discover dependencies from all sources (abstract step).
121
+
122
+ Implementations should discover from:
123
+ - pyproject.toml
124
+ - requirements.txt
125
+ - setup.py
126
+ - custom config files
127
+ """
128
+ pass
129
+
130
+ @abstractmethod
131
+ def _is_cache_valid(self) -> bool:
132
+ """
133
+ Check if cached dependencies are still valid (abstract step).
134
+
135
+ Returns:
136
+ True if cache is valid, False otherwise
137
+ """
138
+ pass
139
+
140
+ @abstractmethod
141
+ def _add_common_mappings(self) -> None:
142
+ """Add common import -> package mappings (abstract step)."""
143
+ pass
144
+
145
+ @abstractmethod
146
+ def _update_file_mtimes(self) -> None:
147
+ """Update file modification times for cache validation (abstract step)."""
148
+ pass
149
+
150
+ def get_discovery_sources(self) -> List[str]:
151
+ """Get list of sources used for discovery."""
152
+ return self._discovery_sources.copy()
153
+
154
+
155
+ # =============================================================================
156
+ # ABSTRACT INSTALLER (Strategy Pattern)
157
+ # =============================================================================
158
+
159
+ class APackageInstaller(IPackageInstaller, ABC):
160
+ """
161
+ Abstract base for package installation.
162
+
163
+ Implements Strategy pattern for different installation modes:
164
+ - AUTO: Automatically install without asking
165
+ - INTERACTIVE: Ask user before installing
166
+ - WARN: Log warning but don't install
167
+ - DISABLED: Don't install anything
168
+ - DRY_RUN: Show what would be installed but don't install
169
+ """
170
+
171
+ __slots__ = ('_package_name', '_enabled', '_mode', '_installed_packages',
172
+ '_failed_packages', '_lock')
173
+
174
+ def __init__(self, package_name: str = 'default'):
175
+ """
176
+ Initialize package installer.
177
+
178
+ Args:
179
+ package_name: Name of package this installer is for (for isolation)
180
+ """
181
+ self._package_name = package_name
182
+ self._enabled = False
183
+ self._mode = LazyInstallMode.AUTO
184
+ self._installed_packages: Set[str] = set()
185
+ self._failed_packages: Set[str] = set()
186
+ self._lock = threading.RLock()
187
+
188
+ def get_package_name(self) -> str:
189
+ """Get the package name this installer is for."""
190
+ return self._package_name
191
+
192
+ def set_mode(self, mode: LazyInstallMode) -> None:
193
+ """Set the installation mode."""
194
+ with self._lock:
195
+ self._mode = mode
196
+
197
+ def get_mode(self) -> LazyInstallMode:
198
+ """Get the current installation mode."""
199
+ return self._mode
200
+
201
+ def enable(self) -> None:
202
+ """Enable lazy installation."""
203
+ with self._lock:
204
+ self._enabled = True
205
+
206
+ def disable(self) -> None:
207
+ """Disable lazy installation."""
208
+ with self._lock:
209
+ self._enabled = False
210
+
211
+ def is_enabled(self) -> bool:
212
+ """Check if lazy installation is enabled."""
213
+ return self._enabled
214
+
215
+ @abstractmethod
216
+ def install_package(self, package_name: str, module_name: str = None) -> bool:
217
+ """
218
+ Install a package (abstract method).
219
+
220
+ Args:
221
+ package_name: Name of package to install
222
+ module_name: Name of module being imported (for interactive mode)
223
+
224
+ Returns:
225
+ True if installation successful, False otherwise
226
+ """
227
+ pass
228
+
229
+ @abstractmethod
230
+ def _check_security_policy(self, package_name: str) -> Tuple[bool, str]:
231
+ """
232
+ Check security policy for package (abstract method).
233
+
234
+ Args:
235
+ package_name: Package to check
236
+
237
+ Returns:
238
+ Tuple of (allowed: bool, reason: str)
239
+ """
240
+ pass
241
+
242
+ @abstractmethod
243
+ def _run_pip_install(self, package_name: str, args: List[str]) -> bool:
244
+ """
245
+ Run pip install with arguments (abstract method).
246
+
247
+ Args:
248
+ package_name: Package to install
249
+ args: Additional pip arguments
250
+
251
+ Returns:
252
+ True if successful, False otherwise
253
+ """
254
+ pass
255
+
256
+ def get_stats(self) -> Dict[str, Any]:
257
+ """Get installation statistics."""
258
+ with self._lock:
259
+ return {
260
+ 'enabled': self._enabled,
261
+ 'mode': self._mode.value,
262
+ 'package_name': self._package_name,
263
+ 'installed_packages': list(self._installed_packages),
264
+ 'failed_packages': list(self._failed_packages),
265
+ 'total_installed': len(self._installed_packages),
266
+ 'total_failed': len(self._failed_packages)
267
+ }
268
+
269
+
270
+ # =============================================================================
271
+ # ABSTRACT IMPORT HOOK (Observer Pattern)
272
+ # =============================================================================
273
+
274
+ class AImportHook(IImportHook, ABC):
275
+ """
276
+ Abstract base for import hooks.
277
+
278
+ Implements Observer pattern to observe import failures and trigger
279
+ lazy installation when needed.
280
+ """
281
+
282
+ __slots__ = ('_package_name', '_enabled')
283
+
284
+ def __init__(self, package_name: str = 'default'):
285
+ """
286
+ Initialize import hook.
287
+
288
+ Args:
289
+ package_name: Package this hook is for
290
+ """
291
+ self._package_name = package_name
292
+ self._enabled = True
293
+
294
+ def enable(self) -> None:
295
+ """Enable the import hook."""
296
+ self._enabled = True
297
+
298
+ def disable(self) -> None:
299
+ """Disable the import hook."""
300
+ self._enabled = False
301
+
302
+ def is_enabled(self) -> bool:
303
+ """Check if hook is enabled."""
304
+ return self._enabled
305
+
306
+ @abstractmethod
307
+ def install_hook(self) -> None:
308
+ """Install the import hook into sys.meta_path (abstract method)."""
309
+ pass
310
+
311
+ @abstractmethod
312
+ def uninstall_hook(self) -> None:
313
+ """Uninstall the import hook from sys.meta_path (abstract method)."""
314
+ pass
315
+
316
+ @abstractmethod
317
+ def handle_import_error(self, module_name: str) -> Optional[Any]:
318
+ """
319
+ Handle ImportError by attempting to install and re-import (abstract method).
320
+
321
+ Args:
322
+ module_name: Name of module that failed to import
323
+
324
+ Returns:
325
+ Imported module if successful, None otherwise
326
+ """
327
+ pass
328
+
329
+
330
+ # =============================================================================
331
+ # ABSTRACT CACHE (Proxy Pattern)
332
+ # =============================================================================
333
+
334
+ class APackageCache(IPackageCache, ABC):
335
+ """
336
+ Abstract base for package caching.
337
+
338
+ Implements Proxy pattern to provide cached access to packages
339
+ and avoid repeated operations.
340
+ """
341
+
342
+ __slots__ = ('_cache', '_lock')
343
+
344
+ def __init__(self):
345
+ """Initialize package cache."""
346
+ self._cache: Dict[str, Any] = {}
347
+ self._lock = threading.RLock()
348
+
349
+ @abstractmethod
350
+ def get_cached(self, key: str) -> Optional[Any]:
351
+ """
352
+ Get cached value (abstract method).
353
+
354
+ Args:
355
+ key: Cache key
356
+
357
+ Returns:
358
+ Cached value or None if not found
359
+ """
360
+ pass
361
+
362
+ @abstractmethod
363
+ def set_cached(self, key: str, value: Any) -> None:
364
+ """
365
+ Set cached value (abstract method).
366
+
367
+ Args:
368
+ key: Cache key
369
+ value: Value to cache
370
+ """
371
+ pass
372
+
373
+ def clear_cache(self) -> None:
374
+ """Clear all cached values."""
375
+ with self._lock:
376
+ self._cache.clear()
377
+
378
+ @abstractmethod
379
+ def is_cache_valid(self, key: str) -> bool:
380
+ """
381
+ Check if cache entry is still valid (abstract method).
382
+
383
+ Args:
384
+ key: Cache key
385
+
386
+ Returns:
387
+ True if valid, False otherwise
388
+ """
389
+ pass
390
+
391
+
392
+ # =============================================================================
393
+ # ABSTRACT LAZY LOADER (Proxy Pattern)
394
+ # =============================================================================
395
+
396
+ class ALazyLoader(ILazyLoader, ABC):
397
+ """
398
+ Abstract base for lazy loading.
399
+
400
+ Implements Proxy pattern to defer module loading until first access.
401
+ """
402
+
403
+ __slots__ = ('_module_path', '_cached_module', '_lock', '_loading')
404
+
405
+ def __init__(self, module_path: str):
406
+ """
407
+ Initialize lazy loader.
408
+
409
+ Args:
410
+ module_path: Full module path to load
411
+ """
412
+ self._module_path = module_path
413
+ self._cached_module: Optional[ModuleType] = None
414
+ self._lock = threading.RLock()
415
+ self._loading = False
416
+
417
+ @abstractmethod
418
+ def load_module(self, module_path: str) -> ModuleType:
419
+ """
420
+ Load a module lazily (abstract method).
421
+
422
+ Args:
423
+ module_path: Full module path to load
424
+
425
+ Returns:
426
+ Loaded module
427
+ """
428
+ pass
429
+
430
+ def is_loaded(self, module_path: str = None) -> bool:
431
+ """
432
+ Check if module is already loaded.
433
+
434
+ Args:
435
+ module_path: Module path to check (uses self._module_path if None)
436
+
437
+ Returns:
438
+ True if loaded, False otherwise
439
+ """
440
+ return self._cached_module is not None
441
+
442
+ @abstractmethod
443
+ def unload_module(self, module_path: str) -> None:
444
+ """
445
+ Unload a module from cache (abstract method).
446
+
447
+ Args:
448
+ module_path: Module path to unload
449
+ """
450
+ pass
451
+
452
+
453
+ # =============================================================================
454
+ # EXPORT ALL
455
+ # =============================================================================
456
+
457
+ __all__ = [
458
+ # Abstract base classes
459
+ 'APackageDiscovery',
460
+ 'APackageInstaller',
461
+ 'AImportHook',
462
+ 'APackageCache',
463
+ 'ALazyLoader',
464
+ ]
465
+