exonware-xwlazy 0.1.0.22__py3-none-any.whl → 0.1.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/__init__.py +20 -1
- exonware/xwlazy/__init__.py +14 -2
- exonware/xwlazy/common/__init__.py +8 -0
- exonware/xwlazy/common/base.py +11 -2
- exonware/xwlazy/common/cache.py +5 -5
- exonware/xwlazy/common/logger.py +5 -5
- exonware/xwlazy/common/services/dependency_mapper.py +31 -13
- exonware/xwlazy/common/services/install_async_utils.py +5 -0
- exonware/xwlazy/common/services/install_cache_utils.py +4 -4
- exonware/xwlazy/common/services/spec_cache.py +2 -2
- exonware/xwlazy/common/services/state_manager.py +4 -4
- exonware/xwlazy/common/strategies/caching_dict.py +2 -2
- exonware/xwlazy/common/strategies/caching_lfu.py +2 -2
- exonware/xwlazy/common/strategies/caching_ttl.py +2 -2
- exonware/xwlazy/common/utils.py +142 -0
- exonware/xwlazy/config.py +1 -1
- exonware/xwlazy/contracts.py +162 -25
- exonware/xwlazy/defs.py +15 -15
- exonware/xwlazy/facade.py +175 -29
- exonware/xwlazy/host/__init__.py +8 -0
- exonware/xwlazy/host/conf.py +16 -0
- exonware/xwlazy/module/base.py +61 -4
- exonware/xwlazy/module/facade.py +1 -1
- exonware/xwlazy/module/importer_engine.py +1017 -170
- exonware/xwlazy/module/partial_module_detector.py +275 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +3 -3
- exonware/xwlazy/package/base.py +106 -41
- exonware/xwlazy/package/conf.py +6 -6
- exonware/xwlazy/package/services/config_manager.py +20 -16
- exonware/xwlazy/package/services/discovery.py +81 -16
- exonware/xwlazy/package/services/host_packages.py +41 -6
- exonware/xwlazy/package/services/install_async.py +16 -2
- exonware/xwlazy/package/services/install_cache.py +4 -4
- exonware/xwlazy/package/services/install_policy.py +14 -14
- exonware/xwlazy/package/services/install_registry.py +3 -3
- exonware/xwlazy/package/services/install_sbom.py +1 -1
- exonware/xwlazy/package/services/installer_engine.py +3 -3
- exonware/xwlazy/package/services/lazy_installer.py +102 -17
- exonware/xwlazy/package/services/manifest.py +43 -36
- exonware/xwlazy/package/services/strategy_registry.py +150 -12
- exonware/xwlazy/package/strategies/package_discovery_file.py +2 -2
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +2 -2
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_async.py +3 -3
- exonware/xwlazy/package/strategies/package_execution_cached.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_pip.py +2 -2
- exonware/xwlazy/package/strategies/package_execution_wheel.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +2 -2
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +2 -2
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +4 -4
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +4 -4
- exonware/xwlazy/package/strategies/package_policy_permissive.py +3 -3
- exonware/xwlazy/package/strategies/package_timing_clean.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_full.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_smart.py +2 -2
- exonware/xwlazy/package/strategies/package_timing_temporary.py +2 -2
- exonware/xwlazy/runtime/adaptive_learner.py +7 -7
- exonware/xwlazy/runtime/base.py +14 -14
- exonware/xwlazy/runtime/facade.py +7 -7
- exonware/xwlazy/runtime/intelligent_selector.py +6 -6
- exonware/xwlazy/runtime/metrics.py +6 -6
- exonware/xwlazy/runtime/performance.py +5 -5
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/METADATA +2 -6
- exonware_xwlazy-0.1.0.23.dist-info/RECORD +93 -0
- xwlazy/__init__.py +14 -0
- xwlazy/lazy.py +30 -0
- exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-0.1.0.23.dist-info}/licenses/LICENSE +0 -0
exonware/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ This is a namespace package allowing multiple exonware subpackages
|
|
|
10
10
|
to coexist (xwsystem, xwnode, xwdata, xwlazy, etc.)
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
# Make this a namespace package
|
|
13
|
+
# Make this a namespace package FIRST
|
|
14
14
|
# This allows both exonware.xwsystem and exonware.xwlazy to coexist
|
|
15
15
|
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
16
16
|
|
|
@@ -21,3 +21,22 @@ __author__ = 'Eng. Muhammad AlShehri'
|
|
|
21
21
|
__email__ = 'connect@exonware.com'
|
|
22
22
|
__company__ = 'eXonware.com'
|
|
23
23
|
|
|
24
|
+
# NOW enable lazy mode (after namespace package is set up)
|
|
25
|
+
import sys
|
|
26
|
+
import importlib
|
|
27
|
+
try:
|
|
28
|
+
# Use importlib to import after namespace is ready
|
|
29
|
+
if 'exonware.xwlazy' not in sys.modules:
|
|
30
|
+
xwlazy_module = importlib.import_module('exonware.xwlazy')
|
|
31
|
+
auto_enable_lazy = getattr(xwlazy_module, 'auto_enable_lazy', None)
|
|
32
|
+
if auto_enable_lazy:
|
|
33
|
+
auto_enable_lazy("xwsystem", mode="smart")
|
|
34
|
+
print("✅ Lazy mode enabled for xwsystem")
|
|
35
|
+
else:
|
|
36
|
+
# Module already loaded, use it directly
|
|
37
|
+
from exonware.xwlazy import auto_enable_lazy
|
|
38
|
+
auto_enable_lazy("xwsystem", mode="smart")
|
|
39
|
+
print("✅ Lazy mode enabled for xwsystem")
|
|
40
|
+
except (ImportError, AttributeError):
|
|
41
|
+
print("❌ Lazy mode not enabled for xwsystem (xwlazy not installed)")
|
|
42
|
+
pass # xwlazy not installed - silently continue
|
exonware/xwlazy/__init__.py
CHANGED
|
@@ -63,6 +63,10 @@ from .facade import (
|
|
|
63
63
|
configure_lazy_mode,
|
|
64
64
|
preload_modules,
|
|
65
65
|
optimize_lazy_mode,
|
|
66
|
+
# One-line activation API
|
|
67
|
+
auto_enable_lazy,
|
|
68
|
+
# Lazy-loader compatible API
|
|
69
|
+
attach,
|
|
66
70
|
# Public API functions
|
|
67
71
|
enable_lazy_install,
|
|
68
72
|
disable_lazy_install,
|
|
@@ -167,6 +171,10 @@ def __getattr__(name: str) -> Any:
|
|
|
167
171
|
elif name == "XWRuntimeHelper":
|
|
168
172
|
from .runtime import XWRuntimeHelper
|
|
169
173
|
return XWRuntimeHelper
|
|
174
|
+
elif name == "manifest":
|
|
175
|
+
# Import manifest module for lazy access
|
|
176
|
+
from .package.services import manifest
|
|
177
|
+
return manifest
|
|
170
178
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
171
179
|
|
|
172
180
|
# Import core classes for advanced usage
|
|
@@ -210,7 +218,7 @@ from .module.importer_engine import (
|
|
|
210
218
|
_mark_import_started,
|
|
211
219
|
_mark_import_finished,
|
|
212
220
|
_lazy_aware_import_module,
|
|
213
|
-
_patch_import_module,
|
|
221
|
+
# _patch_import_module removed - deprecated, use sys.meta_path hooks instead
|
|
214
222
|
_unpatch_import_module,
|
|
215
223
|
)
|
|
216
224
|
|
|
@@ -245,6 +253,10 @@ __all__ = [
|
|
|
245
253
|
"configure_lazy_mode",
|
|
246
254
|
"preload_modules",
|
|
247
255
|
"optimize_lazy_mode",
|
|
256
|
+
# One-line activation API
|
|
257
|
+
"auto_enable_lazy",
|
|
258
|
+
# Lazy-loader compatible API
|
|
259
|
+
"attach",
|
|
248
260
|
# Public API functions
|
|
249
261
|
"enable_lazy_install",
|
|
250
262
|
"disable_lazy_install",
|
|
@@ -361,7 +373,7 @@ __all__ = [
|
|
|
361
373
|
"_mark_import_started",
|
|
362
374
|
"_mark_import_finished",
|
|
363
375
|
"_lazy_aware_import_module",
|
|
364
|
-
"_patch_import_module",
|
|
376
|
+
# "_patch_import_module", # Removed - deprecated, use sys.meta_path hooks instead
|
|
365
377
|
"_unpatch_import_module",
|
|
366
378
|
]
|
|
367
379
|
|
|
@@ -28,6 +28,11 @@ from .cache import (
|
|
|
28
28
|
InstallationCache,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
from .utils import (
|
|
32
|
+
find_project_root,
|
|
33
|
+
find_config_file,
|
|
34
|
+
)
|
|
35
|
+
|
|
31
36
|
__all__ = [
|
|
32
37
|
# Logger
|
|
33
38
|
'get_logger',
|
|
@@ -43,5 +48,8 @@ __all__ = [
|
|
|
43
48
|
'MultiTierCache',
|
|
44
49
|
'BytecodeCache',
|
|
45
50
|
'InstallationCache',
|
|
51
|
+
# Utils
|
|
52
|
+
'find_project_root',
|
|
53
|
+
'find_config_file',
|
|
46
54
|
]
|
|
47
55
|
|
exonware/xwlazy/common/base.py
CHANGED
|
@@ -12,13 +12,21 @@ Abstract base classes for shared/common strategies.
|
|
|
12
12
|
|
|
13
13
|
from abc import ABC, abstractmethod
|
|
14
14
|
from typing import Optional, Any
|
|
15
|
-
from ..contracts import ICachingStrategy
|
|
15
|
+
from ..contracts import ICachingStrategy, ICacheStrategy
|
|
16
16
|
|
|
17
17
|
# =============================================================================
|
|
18
18
|
# ABSTRACT CACHING STRATEGY
|
|
19
19
|
# =============================================================================
|
|
20
20
|
|
|
21
21
|
class ACachingStrategy(ICachingStrategy, ABC):
|
|
22
|
+
"""
|
|
23
|
+
Abstract base class for caching strategies (legacy name).
|
|
24
|
+
|
|
25
|
+
Note: Use ACacheStrategy for new code (ICacheStrategy interface).
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class ACacheStrategy(ICacheStrategy, ABC):
|
|
22
30
|
"""
|
|
23
31
|
Abstract base class for caching strategies.
|
|
24
32
|
|
|
@@ -51,6 +59,7 @@ class ACachingStrategy(ICachingStrategy, ABC):
|
|
|
51
59
|
# =============================================================================
|
|
52
60
|
|
|
53
61
|
__all__ = [
|
|
54
|
-
'ACachingStrategy',
|
|
62
|
+
'ACachingStrategy', # Legacy name
|
|
63
|
+
'ACacheStrategy', # New name for ICacheStrategy interface
|
|
55
64
|
]
|
|
56
65
|
|
exonware/xwlazy/common/cache.py
CHANGED
|
@@ -23,7 +23,7 @@ import importlib
|
|
|
23
23
|
import importlib.util
|
|
24
24
|
import threading
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import
|
|
26
|
+
from typing import Optional, Any
|
|
27
27
|
from collections import OrderedDict
|
|
28
28
|
from queue import Queue
|
|
29
29
|
|
|
@@ -59,7 +59,7 @@ class MultiTierCache:
|
|
|
59
59
|
self._l2_dir = l2_dir or Path.home() / ".xwlazy_cache"
|
|
60
60
|
self._l2_dir.mkdir(parents=True, exist_ok=True)
|
|
61
61
|
self._enable_l3 = enable_l3
|
|
62
|
-
self._l3_patterns:
|
|
62
|
+
self._l3_patterns: dict[str, tuple[int, float]] = {}
|
|
63
63
|
self._lock = threading.RLock()
|
|
64
64
|
|
|
65
65
|
self._l2_write_queue: Queue = Queue()
|
|
@@ -192,7 +192,7 @@ class MultiTierCache:
|
|
|
192
192
|
for old_key, _ in sorted_patterns[:1000]:
|
|
193
193
|
del self._l3_patterns[old_key]
|
|
194
194
|
|
|
195
|
-
def get_predictive_keys(self, limit: int = 10) ->
|
|
195
|
+
def get_predictive_keys(self, limit: int = 10) -> list[str]:
|
|
196
196
|
"""
|
|
197
197
|
Get keys likely to be accessed soon (for preloading).
|
|
198
198
|
|
|
@@ -370,7 +370,7 @@ class InstallationCache:
|
|
|
370
370
|
|
|
371
371
|
self._cache_file = cache_file
|
|
372
372
|
self._lock = threading.RLock()
|
|
373
|
-
self._cache:
|
|
373
|
+
self._cache: dict[str, dict[str, Any]] = {}
|
|
374
374
|
self._dirty = False
|
|
375
375
|
|
|
376
376
|
# Load cache on init
|
|
@@ -487,7 +487,7 @@ class InstallationCache:
|
|
|
487
487
|
self._dirty = True
|
|
488
488
|
self._save_cache()
|
|
489
489
|
|
|
490
|
-
def get_all_installed(self) ->
|
|
490
|
+
def get_all_installed(self) -> set[str]:
|
|
491
491
|
"""
|
|
492
492
|
Get set of all packages marked as installed.
|
|
493
493
|
|
exonware/xwlazy/common/logger.py
CHANGED
|
@@ -17,7 +17,7 @@ import os
|
|
|
17
17
|
import sys
|
|
18
18
|
import logging
|
|
19
19
|
import io
|
|
20
|
-
from typing import
|
|
20
|
+
from typing import Optional
|
|
21
21
|
from datetime import datetime
|
|
22
22
|
|
|
23
23
|
# =============================================================================
|
|
@@ -37,7 +37,7 @@ _EMOJI_MAP = {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
# Default log category states
|
|
40
|
-
_CATEGORY_DEFAULTS:
|
|
40
|
+
_CATEGORY_DEFAULTS: dict[str, bool] = {
|
|
41
41
|
"install": True,
|
|
42
42
|
"hook": False,
|
|
43
43
|
"enhance": False,
|
|
@@ -52,7 +52,7 @@ _CATEGORY_DEFAULTS: Dict[str, bool] = {
|
|
|
52
52
|
# =============================================================================
|
|
53
53
|
|
|
54
54
|
_configured = False
|
|
55
|
-
_category_overrides:
|
|
55
|
+
_category_overrides: dict[str, bool] = {}
|
|
56
56
|
|
|
57
57
|
# =============================================================================
|
|
58
58
|
# HELPER FUNCTIONS
|
|
@@ -186,7 +186,7 @@ def set_log_category(category: str, enabled: bool) -> None:
|
|
|
186
186
|
"""
|
|
187
187
|
_category_overrides[_normalize_category(category)] = bool(enabled)
|
|
188
188
|
|
|
189
|
-
def set_log_categories(overrides:
|
|
189
|
+
def set_log_categories(overrides: dict[str, bool]) -> None:
|
|
190
190
|
"""
|
|
191
191
|
Bulk update multiple log categories.
|
|
192
192
|
|
|
@@ -196,7 +196,7 @@ def set_log_categories(overrides: Dict[str, bool]) -> None:
|
|
|
196
196
|
for category, enabled in overrides.items():
|
|
197
197
|
set_log_category(category, enabled)
|
|
198
198
|
|
|
199
|
-
def get_log_categories() ->
|
|
199
|
+
def get_log_categories() -> dict[str, bool]:
|
|
200
200
|
"""
|
|
201
201
|
Return the effective state for each built-in log category.
|
|
202
202
|
|
|
@@ -5,7 +5,7 @@ This module contains DependencyMapper class extracted from lazy_core.py Section
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import threading
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional
|
|
9
9
|
|
|
10
10
|
# Lazy import to avoid circular dependency
|
|
11
11
|
def _get_logger():
|
|
@@ -59,14 +59,14 @@ class DependencyMapper:
|
|
|
59
59
|
def __init__(self, package_name: str = 'default'):
|
|
60
60
|
"""Initialize dependency mapper."""
|
|
61
61
|
self._discovery = None # Lazy init to avoid circular imports
|
|
62
|
-
self._package_import_mapping:
|
|
63
|
-
self._import_package_mapping:
|
|
62
|
+
self._package_import_mapping: dict[str, list[str]] = {}
|
|
63
|
+
self._import_package_mapping: dict[str, str] = {}
|
|
64
64
|
self._cached = False
|
|
65
65
|
self._lock = threading.RLock()
|
|
66
66
|
self._package_name = package_name
|
|
67
67
|
self._manifest_generation = -1
|
|
68
|
-
self._manifest_dependencies:
|
|
69
|
-
self._manifest_signature: Optional[
|
|
68
|
+
self._manifest_dependencies: dict[str, str] = {}
|
|
69
|
+
self._manifest_signature: Optional[tuple[str, float, float]] = None
|
|
70
70
|
self._manifest_empty = False
|
|
71
71
|
|
|
72
72
|
def set_package_name(self, package_name: str) -> None:
|
|
@@ -115,7 +115,7 @@ class DependencyMapper:
|
|
|
115
115
|
manifest = loader.get_manifest(self._package_name)
|
|
116
116
|
current_generation = loader.generation
|
|
117
117
|
|
|
118
|
-
dependencies:
|
|
118
|
+
dependencies: dict[str, str] = {}
|
|
119
119
|
manifest_empty = True
|
|
120
120
|
if manifest and manifest.dependencies:
|
|
121
121
|
dependencies = {
|
|
@@ -143,7 +143,7 @@ class DependencyMapper:
|
|
|
143
143
|
_cache_spec_if_missing(module_name)
|
|
144
144
|
return needs_cache
|
|
145
145
|
|
|
146
|
-
DENY_LIST:
|
|
146
|
+
DENY_LIST: set[str] = {
|
|
147
147
|
# POSIX-only modules that don't exist on Windows but try to auto-install
|
|
148
148
|
"pwd",
|
|
149
149
|
"grp",
|
|
@@ -191,7 +191,9 @@ class DependencyMapper:
|
|
|
191
191
|
1. Skip checks (stdlib, deny list)
|
|
192
192
|
2. Manifest dependencies (explicit user configuration - highest priority)
|
|
193
193
|
3. Spec cache (module already exists - skip auto-install)
|
|
194
|
-
4. Discovery mappings (automatic discovery)
|
|
194
|
+
4. Discovery mappings (automatic discovery from project configs)
|
|
195
|
+
5. Common mappings (quick access list - works without project configs)
|
|
196
|
+
6. Fallback to import_name itself
|
|
195
197
|
"""
|
|
196
198
|
if self._should_skip_auto_install(import_name):
|
|
197
199
|
return None
|
|
@@ -206,24 +208,40 @@ class DependencyMapper:
|
|
|
206
208
|
if manifest_hit:
|
|
207
209
|
return manifest_hit
|
|
208
210
|
|
|
209
|
-
# Check
|
|
211
|
+
# Check common mappings BEFORE spec cache (common mappings are reliable)
|
|
212
|
+
# This is important for exonware projects to work immediately
|
|
213
|
+
# Common mappings take precedence over spec cache because spec cache can be stale
|
|
214
|
+
discovery = self._get_discovery()
|
|
215
|
+
common_mappings = getattr(discovery, 'COMMON_MAPPINGS', {})
|
|
216
|
+
common_hit = common_mappings.get(import_name)
|
|
217
|
+
if common_hit:
|
|
218
|
+
return common_hit
|
|
219
|
+
|
|
220
|
+
# Check spec cache - if module already exists AND we don't have a common mapping, skip auto-install
|
|
221
|
+
# Note: We check this AFTER common mappings because spec cache can be stale after uninstallation
|
|
210
222
|
if _spec_cache_get(import_name):
|
|
211
223
|
return None
|
|
212
224
|
|
|
225
|
+
# Try discovery mappings (from project configs)
|
|
213
226
|
self._ensure_mappings_cached()
|
|
214
|
-
|
|
227
|
+
discovery_hit = self._import_package_mapping.get(import_name)
|
|
228
|
+
if discovery_hit:
|
|
229
|
+
return discovery_hit
|
|
230
|
+
|
|
231
|
+
# Fallback: assume import name matches package name
|
|
232
|
+
return import_name
|
|
215
233
|
|
|
216
|
-
def get_import_names(self, package_name: str) ->
|
|
234
|
+
def get_import_names(self, package_name: str) -> list[str]:
|
|
217
235
|
"""Get all possible import names for a package."""
|
|
218
236
|
self._ensure_mappings_cached()
|
|
219
237
|
return self._package_import_mapping.get(package_name, [package_name])
|
|
220
238
|
|
|
221
|
-
def get_package_import_mapping(self) ->
|
|
239
|
+
def get_package_import_mapping(self) -> dict[str, list[str]]:
|
|
222
240
|
"""Get complete package to import names mapping."""
|
|
223
241
|
self._ensure_mappings_cached()
|
|
224
242
|
return self._package_import_mapping.copy()
|
|
225
243
|
|
|
226
|
-
def get_import_package_mapping(self) ->
|
|
244
|
+
def get_import_package_mapping(self) -> dict[str, str]:
|
|
227
245
|
"""Get complete import to package name mapping."""
|
|
228
246
|
self._ensure_mappings_cached()
|
|
229
247
|
return self._import_package_mapping.copy()
|
|
@@ -31,6 +31,11 @@ def _ensure_logging_initialized():
|
|
|
31
31
|
global logger
|
|
32
32
|
if logger is None:
|
|
33
33
|
logger = _get_logger()
|
|
34
|
+
# Fallback if get_logger returns None (should not happen but safety first)
|
|
35
|
+
if logger is None:
|
|
36
|
+
import logging
|
|
37
|
+
logger = logging.getLogger("xwlazy.install_async_utils.fallback")
|
|
38
|
+
logger.addHandler(logging.NullHandler())
|
|
34
39
|
|
|
35
40
|
async def get_package_size_mb(package_name: str) -> Optional[float]:
|
|
36
41
|
"""
|
|
@@ -19,7 +19,7 @@ import tempfile
|
|
|
19
19
|
import subprocess
|
|
20
20
|
import zipfile
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Optional
|
|
22
|
+
from typing import Optional
|
|
23
23
|
from contextlib import suppress
|
|
24
24
|
|
|
25
25
|
# Lazy imports
|
|
@@ -77,7 +77,7 @@ def get_site_packages_dir() -> Path:
|
|
|
77
77
|
path.mkdir(parents=True, exist_ok=True)
|
|
78
78
|
return path
|
|
79
79
|
|
|
80
|
-
def pip_install_from_path(wheel_path: Path, policy_args: Optional[
|
|
80
|
+
def pip_install_from_path(wheel_path: Path, policy_args: Optional[list[str]] = None) -> bool:
|
|
81
81
|
"""Install a wheel file using pip."""
|
|
82
82
|
try:
|
|
83
83
|
pip_args = [
|
|
@@ -104,7 +104,7 @@ def pip_install_from_path(wheel_path: Path, policy_args: Optional[List[str]] = N
|
|
|
104
104
|
|
|
105
105
|
def ensure_cached_wheel(
|
|
106
106
|
package_name: str,
|
|
107
|
-
policy_args: Optional[
|
|
107
|
+
policy_args: Optional[list[str]] = None,
|
|
108
108
|
cache_dir: Optional[Path] = None
|
|
109
109
|
) -> Optional[Path]:
|
|
110
110
|
"""Ensure a wheel is cached, downloading it if necessary."""
|
|
@@ -220,7 +220,7 @@ def has_cached_install_tree(
|
|
|
220
220
|
|
|
221
221
|
def install_from_cached_wheel(
|
|
222
222
|
package_name: str,
|
|
223
|
-
policy_args: Optional[
|
|
223
|
+
policy_args: Optional[list[str]] = None,
|
|
224
224
|
cache_dir: Optional[Path] = None
|
|
225
225
|
) -> bool:
|
|
226
226
|
"""Install from a cached wheel file."""
|
|
@@ -21,7 +21,7 @@ import importlib
|
|
|
21
21
|
import importlib.machinery
|
|
22
22
|
import importlib.util
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Optional
|
|
24
|
+
from typing import Optional
|
|
25
25
|
from collections import OrderedDict
|
|
26
26
|
from functools import lru_cache
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ _SPEC_CACHE_TTL = float(os.environ.get("XWLAZY_SPEC_CACHE_TTL", "60") or 60.0)
|
|
|
31
31
|
|
|
32
32
|
# Cache storage
|
|
33
33
|
_spec_cache_lock = threading.RLock()
|
|
34
|
-
_spec_cache: OrderedDict[str,
|
|
34
|
+
_spec_cache: OrderedDict[str, tuple[importlib.machinery.ModuleSpec, float]] = OrderedDict()
|
|
35
35
|
|
|
36
36
|
# Multi-level cache: L1 (in-memory) + L2 (disk)
|
|
37
37
|
_CACHE_L2_DIR = Path(
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Optional
|
|
7
7
|
|
|
8
8
|
def _get_base_config_dir() -> Path:
|
|
9
9
|
"""Determine a cross-platform directory for storing lazy configuration."""
|
|
@@ -25,12 +25,12 @@ class LazyStateManager:
|
|
|
25
25
|
def __init__(self, package_name: str) -> None:
|
|
26
26
|
self._package = package_name.lower()
|
|
27
27
|
self._state_path = _get_base_config_dir() / "state.json"
|
|
28
|
-
self._state:
|
|
28
|
+
self._state: dict[str, dict[str, bool]] = self._load_state()
|
|
29
29
|
|
|
30
30
|
# --------------------------------------------------------------------- #
|
|
31
31
|
# Persistence helpers
|
|
32
32
|
# --------------------------------------------------------------------- #
|
|
33
|
-
def _load_state(self) ->
|
|
33
|
+
def _load_state(self) -> dict[str, dict[str, bool]]:
|
|
34
34
|
if not self._state_path.exists():
|
|
35
35
|
return {}
|
|
36
36
|
try:
|
|
@@ -47,7 +47,7 @@ class LazyStateManager:
|
|
|
47
47
|
with self._state_path.open("w", encoding="utf-8") as fh:
|
|
48
48
|
json.dump(self._state, fh, indent=2, sort_keys=True)
|
|
49
49
|
|
|
50
|
-
def _ensure_entry(self) ->
|
|
50
|
+
def _ensure_entry(self) -> dict[str, bool]:
|
|
51
51
|
return self._state.setdefault(self._package, {})
|
|
52
52
|
|
|
53
53
|
# --------------------------------------------------------------------- #
|
|
@@ -11,7 +11,7 @@ Simple dict-based cache implementation.
|
|
|
11
11
|
Works with ANY data type (modules, packages, etc.).
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import Optional, Any
|
|
15
15
|
from ...common.base import ACachingStrategy
|
|
16
16
|
|
|
17
17
|
class DictCache(ACachingStrategy):
|
|
@@ -24,7 +24,7 @@ class DictCache(ACachingStrategy):
|
|
|
24
24
|
|
|
25
25
|
def __init__(self):
|
|
26
26
|
"""Initialize dict cache."""
|
|
27
|
-
self._cache:
|
|
27
|
+
self._cache: dict[str, Any] = {}
|
|
28
28
|
|
|
29
29
|
def get(self, key: str) -> Optional[Any]:
|
|
30
30
|
"""Get value from cache."""
|
|
@@ -11,7 +11,7 @@ LFU cache implementation with size limit.
|
|
|
11
11
|
Works with ANY data type (modules, packages, etc.).
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import Optional, Any
|
|
15
15
|
from collections import Counter
|
|
16
16
|
from ...common.base import ACachingStrategy
|
|
17
17
|
|
|
@@ -30,7 +30,7 @@ class LFUCache(ACachingStrategy):
|
|
|
30
30
|
Args:
|
|
31
31
|
max_size: Maximum number of items in cache
|
|
32
32
|
"""
|
|
33
|
-
self._cache:
|
|
33
|
+
self._cache: dict[str, Any] = {}
|
|
34
34
|
self._freq: Counter[str] = Counter()
|
|
35
35
|
self._max_size = max_size
|
|
36
36
|
|
|
@@ -12,7 +12,7 @@ Works with ANY data type (modules, packages, etc.).
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import time
|
|
15
|
-
from typing import
|
|
15
|
+
from typing import Optional, Any
|
|
16
16
|
from ...common.base import ACachingStrategy
|
|
17
17
|
|
|
18
18
|
class TTLCache(ACachingStrategy):
|
|
@@ -30,7 +30,7 @@ class TTLCache(ACachingStrategy):
|
|
|
30
30
|
Args:
|
|
31
31
|
ttl_seconds: Time-to-live in seconds (default: 1 hour)
|
|
32
32
|
"""
|
|
33
|
-
self._cache:
|
|
33
|
+
self._cache: dict[str, tuple[Any, float]] = {} # (value, expiry_time)
|
|
34
34
|
self._ttl = ttl_seconds
|
|
35
35
|
|
|
36
36
|
def get(self, key: str) -> Optional[Any]:
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utility functions for xwlazy.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
|
|
8
|
+
This module provides shared utility functions used across xwlazy,
|
|
9
|
+
including path operations and project root detection.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_project_root(start_path: Optional[Path] = None) -> Path:
|
|
18
|
+
"""
|
|
19
|
+
Find the project root directory by looking for markers.
|
|
20
|
+
|
|
21
|
+
Package-agnostic: Finds root from running script location, not xwlazy's location.
|
|
22
|
+
This function is designed to work with any project that uses xwlazy.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
start_path: Optional starting path. If None, attempts to find from:
|
|
26
|
+
1. __main__ module location
|
|
27
|
+
2. sys.path[0] (script directory)
|
|
28
|
+
3. Current working directory
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Path to the project root directory
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
>>> root = find_project_root()
|
|
35
|
+
>>> # Finds pyproject.toml or setup.py from running script
|
|
36
|
+
|
|
37
|
+
>>> root = find_project_root(Path(__file__).parent)
|
|
38
|
+
>>> # Finds from specific starting path
|
|
39
|
+
"""
|
|
40
|
+
start_paths = []
|
|
41
|
+
|
|
42
|
+
# If explicit start path provided, use it first
|
|
43
|
+
if start_path:
|
|
44
|
+
try:
|
|
45
|
+
start_paths.append(start_path.resolve())
|
|
46
|
+
except (ValueError, OSError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Option 1: Use __main__ module location if available
|
|
50
|
+
if '__main__' in sys.modules:
|
|
51
|
+
main_module = sys.modules['__main__']
|
|
52
|
+
if hasattr(main_module, '__file__') and main_module.__file__:
|
|
53
|
+
try:
|
|
54
|
+
main_path = Path(main_module.__file__).resolve().parent
|
|
55
|
+
start_paths.append(main_path)
|
|
56
|
+
except (ValueError, OSError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Option 2: Use sys.path[0] (script directory)
|
|
60
|
+
if sys.path and sys.path[0]:
|
|
61
|
+
try:
|
|
62
|
+
script_path = Path(sys.path[0]).resolve()
|
|
63
|
+
if script_path.exists():
|
|
64
|
+
start_paths.append(script_path)
|
|
65
|
+
except (ValueError, OSError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Option 3: Use current working directory
|
|
69
|
+
try:
|
|
70
|
+
cwd = Path.cwd().resolve()
|
|
71
|
+
if cwd.exists():
|
|
72
|
+
start_paths.append(cwd)
|
|
73
|
+
except (OSError, ValueError):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
# Option 4: Fallback to xwlazy location (for backwards compatibility)
|
|
77
|
+
try:
|
|
78
|
+
xwlazy_path = Path(__file__).parent.parent.parent.parent
|
|
79
|
+
if xwlazy_path.exists():
|
|
80
|
+
start_paths.append(xwlazy_path)
|
|
81
|
+
except (ValueError, OSError):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# Try each starting path
|
|
85
|
+
for start_path_item in start_paths:
|
|
86
|
+
current = start_path_item
|
|
87
|
+
max_levels = 20 # Prevent infinite loops
|
|
88
|
+
levels = 0
|
|
89
|
+
|
|
90
|
+
while current != current.parent and levels < max_levels:
|
|
91
|
+
# Check for project markers
|
|
92
|
+
markers = ['pyproject.toml', 'setup.py', 'requirements.txt', '.git']
|
|
93
|
+
if any((current / marker).exists() for marker in markers):
|
|
94
|
+
return current
|
|
95
|
+
current = current.parent
|
|
96
|
+
levels += 1
|
|
97
|
+
|
|
98
|
+
# Final fallback: current working directory
|
|
99
|
+
try:
|
|
100
|
+
return Path.cwd().resolve()
|
|
101
|
+
except (OSError, ValueError):
|
|
102
|
+
# Ultimate fallback: return the xwlazy package directory
|
|
103
|
+
return Path(__file__).parent.parent.parent.parent
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_config_file(filename: str, start_path: Optional[Path] = None) -> Optional[Path]:
|
|
107
|
+
"""
|
|
108
|
+
Find a configuration file by walking up from the start path.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
filename: Name of the config file to find (e.g., 'pyproject.toml', 'requirements.txt')
|
|
112
|
+
start_path: Optional starting path. If None, uses find_project_root()
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Path to the config file if found, None otherwise
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
>>> pyproject = find_config_file('pyproject.toml')
|
|
119
|
+
>>> requirements = find_config_file('requirements.txt')
|
|
120
|
+
"""
|
|
121
|
+
if start_path is None:
|
|
122
|
+
root = find_project_root()
|
|
123
|
+
else:
|
|
124
|
+
root = start_path
|
|
125
|
+
|
|
126
|
+
# Check in root and walk up
|
|
127
|
+
current = root
|
|
128
|
+
max_levels = 10
|
|
129
|
+
levels = 0
|
|
130
|
+
|
|
131
|
+
while current != current.parent and levels < max_levels:
|
|
132
|
+
config_path = current / filename
|
|
133
|
+
if config_path.exists():
|
|
134
|
+
return config_path
|
|
135
|
+
current = current.parent
|
|
136
|
+
levels += 1
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = ['find_project_root', 'find_config_file']
|
|
142
|
+
|
exonware/xwlazy/config.py
CHANGED