exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.20__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 +26 -0
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/common/__init__.py +47 -0
- exonware/xwlazy/common/base.py +56 -0
- exonware/xwlazy/common/cache.py +504 -0
- exonware/xwlazy/common/logger.py +257 -0
- exonware/xwlazy/common/services/__init__.py +72 -0
- exonware/xwlazy/common/services/dependency_mapper.py +232 -0
- exonware/xwlazy/common/services/install_async_utils.py +165 -0
- exonware/xwlazy/common/services/install_cache_utils.py +245 -0
- exonware/xwlazy/common/services/keyword_detection.py +283 -0
- exonware/xwlazy/common/services/spec_cache.py +165 -0
- xwlazy/lazy/lazy_state.py → exonware/xwlazy/common/services/state_manager.py +0 -2
- exonware/xwlazy/common/strategies/__init__.py +28 -0
- exonware/xwlazy/common/strategies/caching_dict.py +44 -0
- exonware/xwlazy/common/strategies/caching_installation.py +88 -0
- exonware/xwlazy/common/strategies/caching_lfu.py +66 -0
- exonware/xwlazy/common/strategies/caching_lru.py +63 -0
- exonware/xwlazy/common/strategies/caching_multitier.py +59 -0
- exonware/xwlazy/common/strategies/caching_ttl.py +59 -0
- {xwlazy/lazy → exonware/xwlazy}/config.py +51 -21
- exonware/xwlazy/contracts.py +1396 -0
- exonware/xwlazy/defs.py +378 -0
- xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -16
- exonware/xwlazy/facade.py +991 -0
- exonware/xwlazy/module/__init__.py +18 -0
- exonware/xwlazy/module/base.py +565 -0
- exonware/xwlazy/module/data.py +17 -0
- exonware/xwlazy/module/facade.py +246 -0
- exonware/xwlazy/module/importer_engine.py +2117 -0
- exonware/xwlazy/module/strategies/__init__.py +22 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +93 -0
- exonware/xwlazy/module/strategies/module_helper_simple.py +65 -0
- exonware/xwlazy/module/strategies/module_manager_advanced.py +111 -0
- exonware/xwlazy/module/strategies/module_manager_simple.py +95 -0
- exonware/xwlazy/package/__init__.py +18 -0
- exonware/xwlazy/package/base.py +798 -0
- xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +61 -16
- exonware/xwlazy/package/data.py +17 -0
- exonware/xwlazy/package/facade.py +480 -0
- exonware/xwlazy/package/services/__init__.py +84 -0
- exonware/xwlazy/package/services/async_install_handle.py +87 -0
- exonware/xwlazy/package/services/config_manager.py +245 -0
- exonware/xwlazy/package/services/discovery.py +370 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -20
- exonware/xwlazy/package/services/install_async.py +277 -0
- exonware/xwlazy/package/services/install_cache.py +145 -0
- exonware/xwlazy/package/services/install_interactive.py +59 -0
- exonware/xwlazy/package/services/install_policy.py +156 -0
- exonware/xwlazy/package/services/install_registry.py +54 -0
- exonware/xwlazy/package/services/install_result.py +17 -0
- exonware/xwlazy/package/services/install_sbom.py +153 -0
- exonware/xwlazy/package/services/install_utils.py +79 -0
- exonware/xwlazy/package/services/installer_engine.py +406 -0
- exonware/xwlazy/package/services/lazy_installer.py +718 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +40 -33
- exonware/xwlazy/package/services/strategy_registry.py +186 -0
- exonware/xwlazy/package/strategies/__init__.py +57 -0
- exonware/xwlazy/package/strategies/package_discovery_file.py +129 -0
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +84 -0
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +101 -0
- exonware/xwlazy/package/strategies/package_execution_async.py +113 -0
- exonware/xwlazy/package/strategies/package_execution_cached.py +90 -0
- exonware/xwlazy/package/strategies/package_execution_pip.py +99 -0
- exonware/xwlazy/package/strategies/package_execution_wheel.py +106 -0
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +100 -0
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +105 -0
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +100 -0
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +57 -0
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +57 -0
- exonware/xwlazy/package/strategies/package_policy_permissive.py +46 -0
- exonware/xwlazy/package/strategies/package_timing_clean.py +67 -0
- exonware/xwlazy/package/strategies/package_timing_full.py +66 -0
- exonware/xwlazy/package/strategies/package_timing_smart.py +68 -0
- exonware/xwlazy/package/strategies/package_timing_temporary.py +66 -0
- exonware/xwlazy/runtime/__init__.py +18 -0
- exonware/xwlazy/runtime/adaptive_learner.py +129 -0
- exonware/xwlazy/runtime/base.py +274 -0
- exonware/xwlazy/runtime/facade.py +94 -0
- exonware/xwlazy/runtime/intelligent_selector.py +170 -0
- exonware/xwlazy/runtime/metrics.py +60 -0
- exonware/xwlazy/runtime/performance.py +37 -0
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/METADATA +89 -11
- exonware_xwlazy-0.1.0.20.dist-info/RECORD +87 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
- xwlazy/__init__.py +0 -34
- xwlazy/lazy/__init__.py +0 -301
- xwlazy/lazy/bootstrap.py +0 -106
- xwlazy/lazy/lazy_base.py +0 -465
- xwlazy/lazy/lazy_contracts.py +0 -290
- xwlazy/lazy/lazy_core.py +0 -3727
- xwlazy/lazy/logging_utils.py +0 -194
- xwlazy/version.py +0 -77
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
#exonware/xwlazy/src/exonware/xwlazy/package/conf.py
|
|
3
|
+
|
|
4
|
+
Host-facing configuration helpers for enabling lazy mode via `exonware.conf`.
|
|
2
5
|
|
|
3
6
|
This module centralizes the legacy configuration surface so host packages no
|
|
4
7
|
longer need to ship their own lazy bootstrap logic. Consumers import
|
|
5
8
|
``exonware.conf`` as before, while the real implementation now lives in
|
|
6
|
-
``xwlazy.
|
|
9
|
+
``xwlazy.package.conf`` to keep lazy concerns within the xwlazy project.
|
|
10
|
+
|
|
11
|
+
Company: eXonware.com
|
|
12
|
+
Author: Eng. Muhammad AlShehri
|
|
13
|
+
Email: connect@exonware.com
|
|
14
|
+
|
|
15
|
+
Generation Date: 10-Oct-2025
|
|
7
16
|
"""
|
|
8
17
|
|
|
9
18
|
from __future__ import annotations
|
|
@@ -16,15 +25,19 @@ import types
|
|
|
16
25
|
import warnings
|
|
17
26
|
from typing import Any, Dict, Optional
|
|
18
27
|
|
|
19
|
-
from
|
|
20
|
-
from .
|
|
28
|
+
# Import from new structure
|
|
29
|
+
from .services.host_packages import refresh_host_package
|
|
30
|
+
from ..facade import (
|
|
21
31
|
config_package_lazy_install_enabled,
|
|
22
32
|
install_import_hook,
|
|
23
33
|
uninstall_import_hook,
|
|
24
34
|
is_import_hook_installed,
|
|
25
35
|
is_lazy_install_enabled,
|
|
26
36
|
)
|
|
37
|
+
from ..defs import get_preset_mode
|
|
38
|
+
from .services.config_manager import LazyInstallConfig
|
|
27
39
|
|
|
40
|
+
__all__ = ['get_conf_module', '_PackageConfig', '_FilteredStderr', '_LazyConfModule', '_setup_global_warning_filter']
|
|
28
41
|
|
|
29
42
|
class _PackageConfig:
|
|
30
43
|
"""Per-package configuration wrapper."""
|
|
@@ -42,7 +55,8 @@ class _PackageConfig:
|
|
|
42
55
|
def lazy_install(self, value: bool) -> None:
|
|
43
56
|
"""Enable/disable lazy mode for this package."""
|
|
44
57
|
if value:
|
|
45
|
-
|
|
58
|
+
# Default to "smart" mode when enabling lazy install
|
|
59
|
+
config_package_lazy_install_enabled(self._package_name, True, mode="smart", install_hook=False)
|
|
46
60
|
install_import_hook(self._package_name)
|
|
47
61
|
refresh_host_package(self._package_name)
|
|
48
62
|
else:
|
|
@@ -63,7 +77,6 @@ class _PackageConfig:
|
|
|
63
77
|
"""Return True if lazy install + hook are active."""
|
|
64
78
|
return self.lazy_install_status()["active"]
|
|
65
79
|
|
|
66
|
-
|
|
67
80
|
class _FilteredStderr:
|
|
68
81
|
"""Stderr wrapper that filters out specific warning messages."""
|
|
69
82
|
|
|
@@ -94,7 +107,6 @@ class _FilteredStderr:
|
|
|
94
107
|
"""Delegate all other attributes to original stderr."""
|
|
95
108
|
return getattr(self._original, name)
|
|
96
109
|
|
|
97
|
-
|
|
98
110
|
class _LazyConfModule(types.ModuleType):
|
|
99
111
|
"""Configuration module for all exonware packages."""
|
|
100
112
|
|
|
@@ -120,7 +132,7 @@ class _LazyConfModule(types.ModuleType):
|
|
|
120
132
|
return False
|
|
121
133
|
except Exception:
|
|
122
134
|
try:
|
|
123
|
-
import xwlazy # noqa: F401
|
|
135
|
+
import exonware.xwlazy # noqa: F401
|
|
124
136
|
return True
|
|
125
137
|
except ImportError:
|
|
126
138
|
return False
|
|
@@ -205,6 +217,21 @@ class _LazyConfModule(types.ModuleType):
|
|
|
205
217
|
|
|
206
218
|
if name == "lazy_install":
|
|
207
219
|
return self._is_xwlazy_installed()
|
|
220
|
+
if name == "lazy":
|
|
221
|
+
# Return current lazy mode setting
|
|
222
|
+
# Check if any package has lazy enabled and return its mode
|
|
223
|
+
for pkg_name in package_names:
|
|
224
|
+
if is_lazy_install_enabled(pkg_name):
|
|
225
|
+
mode_config = LazyInstallConfig.get_mode_config(pkg_name)
|
|
226
|
+
if mode_config:
|
|
227
|
+
# Return preset name if matches, otherwise return mode string
|
|
228
|
+
from ..defs import PRESET_MODES
|
|
229
|
+
for preset_name, preset_config in PRESET_MODES.items():
|
|
230
|
+
if (preset_config.load_mode == mode_config.load_mode and
|
|
231
|
+
preset_config.install_mode == mode_config.install_mode):
|
|
232
|
+
return preset_name
|
|
233
|
+
return LazyInstallConfig.get_mode(pkg_name)
|
|
234
|
+
return "none"
|
|
208
235
|
if name == "lazy_install_status":
|
|
209
236
|
return self._get_global_lazy_status
|
|
210
237
|
if name == "is_lazy_active":
|
|
@@ -221,23 +248,45 @@ class _LazyConfModule(types.ModuleType):
|
|
|
221
248
|
if name == "lazy_install":
|
|
222
249
|
if value:
|
|
223
250
|
self._ensure_xwlazy_installed()
|
|
224
|
-
|
|
251
|
+
# Enable with "smart" mode by default
|
|
252
|
+
package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
|
|
253
|
+
for pkg_name in package_names:
|
|
254
|
+
config_package_lazy_install_enabled(pkg_name, True, mode="smart", install_hook=True)
|
|
225
255
|
else:
|
|
226
|
-
|
|
256
|
+
package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
|
|
257
|
+
for pkg_name in package_names:
|
|
258
|
+
config_package_lazy_install_enabled(pkg_name, False, install_hook=False)
|
|
259
|
+
uninstall_import_hook(pkg_name)
|
|
227
260
|
self._uninstall_xwlazy()
|
|
228
261
|
return
|
|
262
|
+
if name == "lazy":
|
|
263
|
+
# Support exonware.conf.lazy = "lite"/"smart"/"full"/"clean"/"auto"
|
|
264
|
+
mode_map = {
|
|
265
|
+
"lite": "lite",
|
|
266
|
+
"smart": "smart",
|
|
267
|
+
"full": "full",
|
|
268
|
+
"clean": "clean",
|
|
269
|
+
"auto": "auto",
|
|
270
|
+
"temporary": "temporary",
|
|
271
|
+
"size_aware": "size_aware",
|
|
272
|
+
"none": "none",
|
|
273
|
+
}
|
|
274
|
+
mode = mode_map.get(str(value).lower(), "smart") # Default to "smart" instead of "auto"
|
|
275
|
+
# Apply to all known packages
|
|
276
|
+
package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
|
|
277
|
+
for pkg_name in package_names:
|
|
278
|
+
config_package_lazy_install_enabled(pkg_name, True, mode, install_hook=True)
|
|
279
|
+
return
|
|
229
280
|
if name == "suppress_warnings":
|
|
230
281
|
self._suppress_warnings = bool(value)
|
|
231
282
|
self._setup_warning_filter()
|
|
232
283
|
return
|
|
233
284
|
super().__setattr__(name, value)
|
|
234
285
|
|
|
235
|
-
|
|
236
286
|
_CONF_INSTANCE: Optional[_LazyConfModule] = None
|
|
237
287
|
_ORIGINAL_STDERR: Optional[Any] = None
|
|
238
288
|
_FILTERED_STDERR: Optional[_FilteredStderr] = None
|
|
239
289
|
|
|
240
|
-
|
|
241
290
|
def _setup_global_warning_filter() -> None:
|
|
242
291
|
"""Set up global stderr filter for decimal module warnings (called at module import)."""
|
|
243
292
|
global _ORIGINAL_STDERR, _FILTERED_STDERR
|
|
@@ -263,17 +312,13 @@ def _setup_global_warning_filter() -> None:
|
|
|
263
312
|
if sys.stderr is not _FILTERED_STDERR:
|
|
264
313
|
sys.stderr = _FILTERED_STDERR # type: ignore[assignment]
|
|
265
314
|
|
|
266
|
-
|
|
267
315
|
# Set up warning filter immediately when module is imported (default: suppress warnings)
|
|
268
316
|
# Note: conf.py may have already set up a filter, which is fine
|
|
269
317
|
_setup_global_warning_filter()
|
|
270
318
|
|
|
271
|
-
|
|
272
319
|
def get_conf_module(name: str = "exonware.conf", doc: Optional[str] = None) -> types.ModuleType:
|
|
273
320
|
"""Return (and memoize) the shared conf module instance."""
|
|
274
321
|
global _CONF_INSTANCE
|
|
275
322
|
if _CONF_INSTANCE is None:
|
|
276
323
|
_CONF_INSTANCE = _LazyConfModule(name, doc)
|
|
277
324
|
return _CONF_INSTANCE
|
|
278
|
-
|
|
279
|
-
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package Data - Immutable data structure for packages.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
|
|
8
|
+
Generation Date: 15-Nov-2025
|
|
9
|
+
|
|
10
|
+
Re-export PackageData from defs.py for backward compatibility.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Re-export from defs.py
|
|
14
|
+
from ..defs import PackageData
|
|
15
|
+
|
|
16
|
+
__all__ = ['PackageData']
|
|
17
|
+
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package Operations Facade
|
|
3
|
+
|
|
4
|
+
Main facade: XWPackageHelper extends APackageHelper
|
|
5
|
+
Provides concrete implementation for all package operations.
|
|
6
|
+
Uses strategy pattern for caching, helper, and manager strategies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import subprocess
|
|
11
|
+
import importlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .base import APackageHelper
|
|
16
|
+
from .services import InstallerEngine, LazyInstaller
|
|
17
|
+
from .services.strategy_registry import StrategyRegistry
|
|
18
|
+
from ..common.cache import InstallationCache
|
|
19
|
+
from ..common.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger("xwlazy.package.facade")
|
|
22
|
+
|
|
23
|
+
# Import strategy interfaces
|
|
24
|
+
from ..contracts import (
|
|
25
|
+
ICachingStrategy,
|
|
26
|
+
IPackageHelperStrategy,
|
|
27
|
+
IPackageManagerStrategy,
|
|
28
|
+
IInstallExecutionStrategy,
|
|
29
|
+
IInstallTimingStrategy,
|
|
30
|
+
IDiscoveryStrategy,
|
|
31
|
+
IPolicyStrategy,
|
|
32
|
+
IMappingStrategy,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Import default strategies
|
|
36
|
+
from ..common.strategies import InstallationCacheWrapper
|
|
37
|
+
from .strategies import (
|
|
38
|
+
# Execution strategies
|
|
39
|
+
PipExecution,
|
|
40
|
+
# Timing strategies
|
|
41
|
+
SmartTiming,
|
|
42
|
+
# Discovery strategies
|
|
43
|
+
HybridDiscovery,
|
|
44
|
+
# Policy strategies
|
|
45
|
+
PermissivePolicy,
|
|
46
|
+
# Mapping strategies
|
|
47
|
+
ManifestFirstMapping,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
class XWPackageHelper(APackageHelper):
|
|
51
|
+
"""
|
|
52
|
+
Concrete implementation of APackageHelper.
|
|
53
|
+
|
|
54
|
+
Provides simple, clean API for working with packages (what you pip install).
|
|
55
|
+
Uses xwlazy's InstallationCache for persistent caching and LazyInstaller for installation.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
package_name: str = 'default',
|
|
61
|
+
project_root: Optional[str] = None,
|
|
62
|
+
*,
|
|
63
|
+
# Legacy strategy injection (for backward compatibility)
|
|
64
|
+
caching_strategy: Optional[ICachingStrategy] = None,
|
|
65
|
+
helper_strategy: Optional[IPackageHelperStrategy] = None,
|
|
66
|
+
manager_strategy: Optional[IPackageManagerStrategy] = None,
|
|
67
|
+
# New strategy types
|
|
68
|
+
execution_strategy: Optional[IInstallExecutionStrategy] = None,
|
|
69
|
+
timing_strategy: Optional[IInstallTimingStrategy] = None,
|
|
70
|
+
discovery_strategy: Optional[IDiscoveryStrategy] = None,
|
|
71
|
+
policy_strategy: Optional[IPolicyStrategy] = None,
|
|
72
|
+
mapping_strategy: Optional[IMappingStrategy] = None,
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Initialize XW package helper.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
package_name: Package name for isolation (defaults to 'default')
|
|
79
|
+
project_root: Root directory of project (auto-detected if None)
|
|
80
|
+
caching_strategy: Optional caching strategy. If None, uses InstallationCache.
|
|
81
|
+
helper_strategy: Optional helper strategy (legacy, deprecated).
|
|
82
|
+
manager_strategy: Optional manager strategy (legacy, deprecated).
|
|
83
|
+
execution_strategy: Optional execution strategy. If None, uses PipExecution.
|
|
84
|
+
timing_strategy: Optional timing strategy. If None, uses SmartTiming.
|
|
85
|
+
discovery_strategy: Optional discovery strategy. If None, uses HybridDiscovery.
|
|
86
|
+
policy_strategy: Optional policy strategy. If None, uses PermissivePolicy.
|
|
87
|
+
mapping_strategy: Optional mapping strategy. If None, uses ManifestFirstMapping.
|
|
88
|
+
"""
|
|
89
|
+
super().__init__(package_name, project_root)
|
|
90
|
+
|
|
91
|
+
# Default strategies (legacy - deprecated, kept for backward compatibility)
|
|
92
|
+
if caching_strategy is None:
|
|
93
|
+
caching_strategy = InstallationCacheWrapper()
|
|
94
|
+
# Legacy helper_strategy and manager_strategy are deprecated
|
|
95
|
+
# They are kept for backward compatibility but not used
|
|
96
|
+
|
|
97
|
+
# Check registry for stored strategies, otherwise use defaults
|
|
98
|
+
if execution_strategy is None:
|
|
99
|
+
execution_strategy = StrategyRegistry.get_package_strategy(package_name, 'execution')
|
|
100
|
+
if execution_strategy is None:
|
|
101
|
+
execution_strategy = PipExecution()
|
|
102
|
+
if timing_strategy is None:
|
|
103
|
+
timing_strategy = StrategyRegistry.get_package_strategy(package_name, 'timing')
|
|
104
|
+
if timing_strategy is None:
|
|
105
|
+
timing_strategy = SmartTiming()
|
|
106
|
+
if discovery_strategy is None:
|
|
107
|
+
discovery_strategy = StrategyRegistry.get_package_strategy(package_name, 'discovery')
|
|
108
|
+
if discovery_strategy is None:
|
|
109
|
+
discovery_strategy = HybridDiscovery(package_name, project_root)
|
|
110
|
+
if policy_strategy is None:
|
|
111
|
+
policy_strategy = StrategyRegistry.get_package_strategy(package_name, 'policy')
|
|
112
|
+
if policy_strategy is None:
|
|
113
|
+
policy_strategy = PermissivePolicy()
|
|
114
|
+
if mapping_strategy is None:
|
|
115
|
+
mapping_strategy = StrategyRegistry.get_package_strategy(package_name, 'mapping')
|
|
116
|
+
if mapping_strategy is None:
|
|
117
|
+
mapping_strategy = ManifestFirstMapping(package_name)
|
|
118
|
+
|
|
119
|
+
# Store strategies
|
|
120
|
+
self._caching = caching_strategy
|
|
121
|
+
self._helper = helper_strategy # Legacy, deprecated
|
|
122
|
+
self._manager = manager_strategy # Legacy, deprecated
|
|
123
|
+
self._execution = execution_strategy
|
|
124
|
+
self._timing = timing_strategy
|
|
125
|
+
self._discovery = discovery_strategy
|
|
126
|
+
self._policy = policy_strategy
|
|
127
|
+
self._mapping = mapping_strategy
|
|
128
|
+
|
|
129
|
+
# Legacy support (for backward compatibility)
|
|
130
|
+
self._install_cache = InstallationCache()
|
|
131
|
+
self._installer = None # Lazy init to avoid circular imports
|
|
132
|
+
self._install_engine = InstallerEngine(package_name)
|
|
133
|
+
|
|
134
|
+
def _get_installer(self):
|
|
135
|
+
"""Get lazy installer instance (lazy init)."""
|
|
136
|
+
if self._installer is None:
|
|
137
|
+
self._installer = LazyInstaller(self._package_name)
|
|
138
|
+
return self._installer
|
|
139
|
+
|
|
140
|
+
def _check_importability(self, package_name: str) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Check if package is importable.
|
|
143
|
+
|
|
144
|
+
Uses importlib.util.find_spec to check if package can be imported.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
package_name: Package name to check
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if importable, False otherwise
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
spec = importlib.util.find_spec(package_name)
|
|
154
|
+
return spec is not None
|
|
155
|
+
except (ValueError, AttributeError, ImportError):
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def _check_persistent_cache(self, package_name: str) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Check persistent cache for package installation status.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
package_name: Package name to check
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if found in persistent cache as installed, False otherwise
|
|
167
|
+
"""
|
|
168
|
+
return self._install_cache.is_installed(package_name)
|
|
169
|
+
|
|
170
|
+
def _mark_installed_in_persistent_cache(self, package_name: str) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Mark package as installed in persistent cache.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
package_name: Package name to mark
|
|
176
|
+
"""
|
|
177
|
+
version = self._get_installer()._get_installed_version(package_name)
|
|
178
|
+
self._install_cache.mark_installed(package_name, version)
|
|
179
|
+
|
|
180
|
+
def _mark_uninstalled_in_persistent_cache(self, package_name: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Mark package as uninstalled in persistent cache.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
package_name: Package name to mark
|
|
186
|
+
"""
|
|
187
|
+
self._install_cache.mark_uninstalled(package_name)
|
|
188
|
+
|
|
189
|
+
def _run_install(self, *package_names: str) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Run pip install for packages.
|
|
192
|
+
|
|
193
|
+
Uses execution strategy and timing strategy to determine when/how to install.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
*package_names: Package names to install
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
RuntimeError: If installation fails
|
|
200
|
+
"""
|
|
201
|
+
if not package_names:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Get policy args for each package
|
|
205
|
+
policy_args_map = {}
|
|
206
|
+
for package_name in package_names:
|
|
207
|
+
# Check if should install now (timing strategy)
|
|
208
|
+
if not self._timing.should_install_now(package_name, None):
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# Get pip args from policy strategy
|
|
212
|
+
policy_args = self._policy.get_pip_args(package_name)
|
|
213
|
+
policy_args_map[package_name] = policy_args
|
|
214
|
+
|
|
215
|
+
# Execute installations using execution strategy
|
|
216
|
+
for package_name, policy_args in policy_args_map.items():
|
|
217
|
+
result = self._execution.execute_install(package_name, policy_args)
|
|
218
|
+
|
|
219
|
+
# Handle result
|
|
220
|
+
if hasattr(result, 'success') and result.success:
|
|
221
|
+
with self._lock:
|
|
222
|
+
self._installed_packages.add(package_name)
|
|
223
|
+
self._uninstalled_packages.discard(package_name)
|
|
224
|
+
self._mark_installed_in_persistent_cache(package_name)
|
|
225
|
+
else:
|
|
226
|
+
with self._lock:
|
|
227
|
+
self._failed_packages.add(package_name)
|
|
228
|
+
error_msg = getattr(result, 'error', 'Unknown error') if hasattr(result, 'error') else str(result)
|
|
229
|
+
raise RuntimeError(f"Failed to install {package_name}: {error_msg}")
|
|
230
|
+
|
|
231
|
+
def _run_uninstall(self, *package_names: str) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Run pip uninstall for packages.
|
|
234
|
+
|
|
235
|
+
Uses execution strategy for uninstallation.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
*package_names: Package names to uninstall
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
RuntimeError: If uninstallation fails
|
|
242
|
+
"""
|
|
243
|
+
if not package_names:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
for package_name in package_names:
|
|
247
|
+
# Check if should uninstall (timing strategy)
|
|
248
|
+
if self._timing.should_uninstall_after(package_name, None):
|
|
249
|
+
success = self._execution.execute_uninstall(package_name)
|
|
250
|
+
if success:
|
|
251
|
+
with self._lock:
|
|
252
|
+
self._installed_packages.discard(package_name)
|
|
253
|
+
self._uninstalled_packages.add(package_name)
|
|
254
|
+
self._mark_uninstalled_in_persistent_cache(package_name)
|
|
255
|
+
else:
|
|
256
|
+
raise RuntimeError(f"Failed to uninstall {package_name}")
|
|
257
|
+
|
|
258
|
+
# Abstract methods from APackage that need implementation
|
|
259
|
+
def _discover_from_sources(self) -> None:
|
|
260
|
+
"""Discover dependencies from all sources."""
|
|
261
|
+
# Use discovery strategy
|
|
262
|
+
deps = self._discovery.discover(self._project_root)
|
|
263
|
+
# Convert to DependencyInfo format
|
|
264
|
+
from ..defs import DependencyInfo
|
|
265
|
+
for import_name, package_name in deps.items():
|
|
266
|
+
self.discovered_dependencies[import_name] = DependencyInfo(
|
|
267
|
+
import_name=import_name,
|
|
268
|
+
package_name=package_name,
|
|
269
|
+
source=self._discovery.get_source(import_name) or 'discovery',
|
|
270
|
+
category='discovered'
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _is_cache_valid(self) -> bool:
|
|
274
|
+
"""Check if cached dependencies are still valid."""
|
|
275
|
+
# Delegate to discovery strategy which has cache validation logic
|
|
276
|
+
if hasattr(self._discovery, '_is_cache_valid'):
|
|
277
|
+
return self._discovery._is_cache_valid()
|
|
278
|
+
# Fallback: if discovery doesn't support cache validation, assume invalid
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def _add_common_mappings(self) -> None:
|
|
282
|
+
"""Add common import -> package mappings."""
|
|
283
|
+
# Use mapping strategy to discover mappings
|
|
284
|
+
# This is called during initialization to populate common mappings
|
|
285
|
+
# The mapping strategy handles this internally
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
def _update_file_mtimes(self) -> None:
|
|
289
|
+
"""Update file modification times for cache validation."""
|
|
290
|
+
# Delegate to discovery strategy which tracks file modification times
|
|
291
|
+
if hasattr(self._discovery, '_update_file_mtimes'):
|
|
292
|
+
self._discovery._update_file_mtimes()
|
|
293
|
+
|
|
294
|
+
# Strategy swapping methods
|
|
295
|
+
def swap_cache_strategy(self, new_strategy: ICachingStrategy) -> None:
|
|
296
|
+
"""
|
|
297
|
+
Swap cache strategy at runtime.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
new_strategy: New caching strategy to use
|
|
301
|
+
"""
|
|
302
|
+
self._caching = new_strategy
|
|
303
|
+
# Update manager if it uses caching
|
|
304
|
+
if hasattr(self._manager, '_caching'):
|
|
305
|
+
self._manager._caching = new_strategy
|
|
306
|
+
|
|
307
|
+
def swap_helper_strategy(self, new_strategy: IPackageHelperStrategy) -> None:
|
|
308
|
+
"""
|
|
309
|
+
Swap helper/installer strategy at runtime.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
new_strategy: New helper strategy to use
|
|
313
|
+
"""
|
|
314
|
+
self._helper = new_strategy
|
|
315
|
+
# Update manager if it uses helper
|
|
316
|
+
if hasattr(self._manager, '_helper'):
|
|
317
|
+
self._manager._helper = new_strategy
|
|
318
|
+
|
|
319
|
+
def swap_manager_strategy(self, new_strategy: IPackageManagerStrategy) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Swap manager strategy at runtime.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
new_strategy: New manager strategy to use
|
|
325
|
+
"""
|
|
326
|
+
self._manager = new_strategy
|
|
327
|
+
|
|
328
|
+
def swap_execution_strategy(self, new_strategy: IInstallExecutionStrategy) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Swap execution strategy at runtime.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
new_strategy: New execution strategy to use
|
|
334
|
+
"""
|
|
335
|
+
self._execution = new_strategy
|
|
336
|
+
|
|
337
|
+
def swap_timing_strategy(self, new_strategy: IInstallTimingStrategy) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Swap timing strategy at runtime.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
new_strategy: New timing strategy to use
|
|
343
|
+
"""
|
|
344
|
+
self._timing = new_strategy
|
|
345
|
+
|
|
346
|
+
def swap_discovery_strategy(self, new_strategy: IDiscoveryStrategy) -> None:
|
|
347
|
+
"""
|
|
348
|
+
Swap discovery strategy at runtime.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
new_strategy: New discovery strategy to use
|
|
352
|
+
"""
|
|
353
|
+
self._discovery = new_strategy
|
|
354
|
+
|
|
355
|
+
def swap_policy_strategy(self, new_strategy: IPolicyStrategy) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Swap policy strategy at runtime.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
new_strategy: New policy strategy to use
|
|
361
|
+
"""
|
|
362
|
+
self._policy = new_strategy
|
|
363
|
+
|
|
364
|
+
def swap_mapping_strategy(self, new_strategy: IMappingStrategy) -> None:
|
|
365
|
+
"""
|
|
366
|
+
Swap mapping strategy at runtime.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
new_strategy: New mapping strategy to use
|
|
370
|
+
"""
|
|
371
|
+
self._mapping = new_strategy
|
|
372
|
+
|
|
373
|
+
def install_package(self, package_name: str, module_name: Optional[str] = None) -> bool:
|
|
374
|
+
"""
|
|
375
|
+
Install a package.
|
|
376
|
+
|
|
377
|
+
Uses timing strategy to determine if should install now,
|
|
378
|
+
then uses execution strategy to perform installation.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
package_name: Package name to install
|
|
382
|
+
module_name: Optional module name (for mapping)
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
True if installed successfully, False otherwise
|
|
386
|
+
"""
|
|
387
|
+
# Map module name to package name if needed (using mapping strategy)
|
|
388
|
+
if module_name and not package_name:
|
|
389
|
+
package_name = self._mapping.map_import_to_package(module_name) or module_name
|
|
390
|
+
|
|
391
|
+
# Check timing strategy
|
|
392
|
+
if not self._timing.should_install_now(package_name, {'module_name': module_name}):
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
# Check policy strategy
|
|
396
|
+
allowed, reason = self._policy.is_allowed(package_name)
|
|
397
|
+
if not allowed:
|
|
398
|
+
raise RuntimeError(f"Package {package_name} blocked by policy: {reason}")
|
|
399
|
+
|
|
400
|
+
# Get pip args from policy
|
|
401
|
+
policy_args = self._policy.get_pip_args(package_name)
|
|
402
|
+
|
|
403
|
+
# Execute installation
|
|
404
|
+
result = self._execution.execute_install(package_name, policy_args)
|
|
405
|
+
|
|
406
|
+
# Handle result
|
|
407
|
+
if hasattr(result, 'success') and result.success:
|
|
408
|
+
with self._lock:
|
|
409
|
+
self._installed_packages.add(package_name)
|
|
410
|
+
self._uninstalled_packages.discard(package_name)
|
|
411
|
+
self._mark_installed_in_persistent_cache(package_name)
|
|
412
|
+
return True
|
|
413
|
+
else:
|
|
414
|
+
with self._lock:
|
|
415
|
+
self._failed_packages.add(package_name)
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
def _check_security_policy(self, package_name: str):
|
|
419
|
+
"""Check security policy for package."""
|
|
420
|
+
# Use policy strategy
|
|
421
|
+
return self._policy.is_allowed(package_name)
|
|
422
|
+
|
|
423
|
+
def _run_pip_install(self, package_name: str, args: list) -> bool:
|
|
424
|
+
"""Run pip install with arguments."""
|
|
425
|
+
try:
|
|
426
|
+
self._run_install(package_name)
|
|
427
|
+
return True
|
|
428
|
+
except (RuntimeError, subprocess.CalledProcessError, OSError) as e:
|
|
429
|
+
logger.debug(f"Failed to install {package_name}: {e}")
|
|
430
|
+
return False
|
|
431
|
+
except Exception as e:
|
|
432
|
+
# Catch-all for unexpected errors, but log them
|
|
433
|
+
logger.warning(f"Unexpected error installing {package_name}: {e}")
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
def is_cache_valid(self, key: str) -> bool:
|
|
437
|
+
"""Check if cache entry is still valid."""
|
|
438
|
+
# Use caching strategy if it supports validation
|
|
439
|
+
if hasattr(self._caching, 'is_valid') and self._caching is not None:
|
|
440
|
+
return self._caching.is_valid(key)
|
|
441
|
+
# Fallback: check if key exists in cache
|
|
442
|
+
return self.get_cached(key) is not None
|
|
443
|
+
|
|
444
|
+
# IConfigManager methods (delegate to LazyInstallConfig)
|
|
445
|
+
def is_enabled(self, package_name: str) -> bool:
|
|
446
|
+
"""
|
|
447
|
+
Check if lazy install is enabled for a package (from IConfigManager).
|
|
448
|
+
|
|
449
|
+
This method delegates to LazyInstallConfig to avoid method name conflict
|
|
450
|
+
with the instance method is_enabled().
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
package_name: Package name to check
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
True if enabled, False otherwise
|
|
457
|
+
"""
|
|
458
|
+
from .services.config_manager import LazyInstallConfig
|
|
459
|
+
return LazyInstallConfig.is_enabled(package_name)
|
|
460
|
+
|
|
461
|
+
def get_mode(self, package_name: str) -> str:
|
|
462
|
+
"""Get installation mode for a package (from IConfigManager)."""
|
|
463
|
+
from .services.config_manager import LazyInstallConfig
|
|
464
|
+
return LazyInstallConfig.get_mode(package_name)
|
|
465
|
+
|
|
466
|
+
def get_load_mode(self, package_name: str):
|
|
467
|
+
"""Get load mode for a package (from IConfigManager)."""
|
|
468
|
+
from .services.config_manager import LazyInstallConfig
|
|
469
|
+
return LazyInstallConfig.get_load_mode(package_name)
|
|
470
|
+
|
|
471
|
+
def get_install_mode(self, package_name: str):
|
|
472
|
+
"""Get install mode for a package (from IConfigManager)."""
|
|
473
|
+
from .services.config_manager import LazyInstallConfig
|
|
474
|
+
return LazyInstallConfig.get_install_mode(package_name)
|
|
475
|
+
|
|
476
|
+
def get_mode_config(self, package_name: str):
|
|
477
|
+
"""Get full mode configuration for a package (from IConfigManager)."""
|
|
478
|
+
from .services.config_manager import LazyInstallConfig
|
|
479
|
+
return LazyInstallConfig.get_mode_config(package_name)
|
|
480
|
+
|