exonware-xwlazy 0.1.0.23__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.
- exonware/__init__.py +85 -34
- exonware/xwlazy/version.py +5 -5
- exonware/xwlazy.py +2546 -0
- exonware/xwlazy_external_libs.toml +716 -0
- {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +5 -1
- exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
- exonware/xwlazy/__init__.py +0 -379
- exonware/xwlazy/common/__init__.py +0 -55
- exonware/xwlazy/common/base.py +0 -65
- exonware/xwlazy/common/cache.py +0 -504
- exonware/xwlazy/common/logger.py +0 -257
- exonware/xwlazy/common/services/__init__.py +0 -72
- exonware/xwlazy/common/services/dependency_mapper.py +0 -250
- exonware/xwlazy/common/services/install_async_utils.py +0 -170
- exonware/xwlazy/common/services/install_cache_utils.py +0 -245
- exonware/xwlazy/common/services/keyword_detection.py +0 -283
- exonware/xwlazy/common/services/spec_cache.py +0 -165
- exonware/xwlazy/common/services/state_manager.py +0 -84
- exonware/xwlazy/common/strategies/__init__.py +0 -28
- exonware/xwlazy/common/strategies/caching_dict.py +0 -44
- exonware/xwlazy/common/strategies/caching_installation.py +0 -88
- exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
- exonware/xwlazy/common/strategies/caching_lru.py +0 -63
- exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
- exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
- exonware/xwlazy/common/utils.py +0 -142
- exonware/xwlazy/config.py +0 -193
- exonware/xwlazy/contracts.py +0 -1533
- exonware/xwlazy/defs.py +0 -378
- exonware/xwlazy/errors.py +0 -276
- exonware/xwlazy/facade.py +0 -1137
- exonware/xwlazy/host/__init__.py +0 -8
- exonware/xwlazy/host/conf.py +0 -16
- exonware/xwlazy/module/__init__.py +0 -18
- exonware/xwlazy/module/base.py +0 -622
- exonware/xwlazy/module/data.py +0 -17
- exonware/xwlazy/module/facade.py +0 -246
- exonware/xwlazy/module/importer_engine.py +0 -2964
- exonware/xwlazy/module/partial_module_detector.py +0 -275
- exonware/xwlazy/module/strategies/__init__.py +0 -22
- exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
- exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
- exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
- exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
- exonware/xwlazy/package/__init__.py +0 -18
- exonware/xwlazy/package/base.py +0 -863
- exonware/xwlazy/package/conf.py +0 -324
- exonware/xwlazy/package/data.py +0 -17
- exonware/xwlazy/package/facade.py +0 -480
- exonware/xwlazy/package/services/__init__.py +0 -84
- exonware/xwlazy/package/services/async_install_handle.py +0 -87
- exonware/xwlazy/package/services/config_manager.py +0 -249
- exonware/xwlazy/package/services/discovery.py +0 -435
- exonware/xwlazy/package/services/host_packages.py +0 -180
- exonware/xwlazy/package/services/install_async.py +0 -291
- exonware/xwlazy/package/services/install_cache.py +0 -145
- exonware/xwlazy/package/services/install_interactive.py +0 -59
- exonware/xwlazy/package/services/install_policy.py +0 -156
- exonware/xwlazy/package/services/install_registry.py +0 -54
- exonware/xwlazy/package/services/install_result.py +0 -17
- exonware/xwlazy/package/services/install_sbom.py +0 -153
- exonware/xwlazy/package/services/install_utils.py +0 -79
- exonware/xwlazy/package/services/installer_engine.py +0 -406
- exonware/xwlazy/package/services/lazy_installer.py +0 -803
- exonware/xwlazy/package/services/manifest.py +0 -503
- exonware/xwlazy/package/services/strategy_registry.py +0 -324
- exonware/xwlazy/package/strategies/__init__.py +0 -57
- exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
- exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
- exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
- exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
- exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
- exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
- exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
- exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
- exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
- exonware/xwlazy/runtime/__init__.py +0 -18
- exonware/xwlazy/runtime/adaptive_learner.py +0 -129
- exonware/xwlazy/runtime/base.py +0 -274
- exonware/xwlazy/runtime/facade.py +0 -94
- exonware/xwlazy/runtime/intelligent_selector.py +0 -170
- exonware/xwlazy/runtime/metrics.py +0 -60
- exonware/xwlazy/runtime/performance.py +0 -37
- exonware_xwlazy-0.1.0.23.dist-info/RECORD +0 -93
- xwlazy/__init__.py +0 -14
- xwlazy/lazy.py +0 -30
- {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/licenses/LICENSE +0 -0
exonware/xwlazy/common/logger.py
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
#exonware/xwlazy/src/exonware/xwlazy/common/logger.py
|
|
3
|
-
|
|
4
|
-
Logging utilities for xwlazy - shared across package, module, and runtime.
|
|
5
|
-
|
|
6
|
-
Company: eXonware.com
|
|
7
|
-
Author: Eng. Muhammad AlShehri
|
|
8
|
-
Email: connect@exonware.com
|
|
9
|
-
|
|
10
|
-
Generation Date: 15-Nov-2025
|
|
11
|
-
|
|
12
|
-
This module provides unified logging functionality for all xwlazy components.
|
|
13
|
-
All logging code is centralized here to avoid duplication.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import os
|
|
17
|
-
import sys
|
|
18
|
-
import logging
|
|
19
|
-
import io
|
|
20
|
-
from typing import Optional
|
|
21
|
-
from datetime import datetime
|
|
22
|
-
|
|
23
|
-
# =============================================================================
|
|
24
|
-
# CONSTANTS
|
|
25
|
-
# =============================================================================
|
|
26
|
-
|
|
27
|
-
# Emoji mapping for log flags (shared across formatter and format_message)
|
|
28
|
-
_EMOJI_MAP = {
|
|
29
|
-
"WARN": "⚠️",
|
|
30
|
-
"INFO": "ℹ️",
|
|
31
|
-
"ACTION": "⚙️",
|
|
32
|
-
"SUCCESS": "✅",
|
|
33
|
-
"ERROR": "❌",
|
|
34
|
-
"FAIL": "⛔",
|
|
35
|
-
"DEBUG": "🔍",
|
|
36
|
-
"CRITICAL": "🚨",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# Default log category states
|
|
40
|
-
_CATEGORY_DEFAULTS: dict[str, bool] = {
|
|
41
|
-
"install": True,
|
|
42
|
-
"hook": False,
|
|
43
|
-
"enhance": False,
|
|
44
|
-
"audit": False,
|
|
45
|
-
"sbom": False,
|
|
46
|
-
"config": False,
|
|
47
|
-
"discovery": False,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# =============================================================================
|
|
51
|
-
# MODULE STATE
|
|
52
|
-
# =============================================================================
|
|
53
|
-
|
|
54
|
-
_configured = False
|
|
55
|
-
_category_overrides: dict[str, bool] = {}
|
|
56
|
-
|
|
57
|
-
# =============================================================================
|
|
58
|
-
# HELPER FUNCTIONS
|
|
59
|
-
# =============================================================================
|
|
60
|
-
|
|
61
|
-
def _normalize_category(name: str) -> str:
|
|
62
|
-
"""Normalize category name to lowercase."""
|
|
63
|
-
return name.strip().lower()
|
|
64
|
-
|
|
65
|
-
def _load_env_overrides() -> None:
|
|
66
|
-
"""Load log category overrides from environment variables."""
|
|
67
|
-
for category in _CATEGORY_DEFAULTS:
|
|
68
|
-
env_key = f"XWLAZY_LOG_{category.upper()}"
|
|
69
|
-
env_val = os.getenv(env_key)
|
|
70
|
-
if env_val is None:
|
|
71
|
-
continue
|
|
72
|
-
enabled = env_val.strip().lower() not in {"0", "false", "off", "no"}
|
|
73
|
-
_category_overrides[_normalize_category(category)] = enabled
|
|
74
|
-
|
|
75
|
-
# =============================================================================
|
|
76
|
-
# FORMATTER
|
|
77
|
-
# =============================================================================
|
|
78
|
-
|
|
79
|
-
class XWLazyFormatter(logging.Formatter):
|
|
80
|
-
"""Custom formatter for xwlazy that uses exonware.xwlazy [HH:MM:SS]: [FLAG] format."""
|
|
81
|
-
|
|
82
|
-
LEVEL_FLAGS = {
|
|
83
|
-
logging.DEBUG: "DEBUG",
|
|
84
|
-
logging.INFO: "INFO",
|
|
85
|
-
logging.WARNING: "WARN",
|
|
86
|
-
logging.ERROR: "ERROR",
|
|
87
|
-
logging.CRITICAL: "CRITICAL",
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
def format(self, record: logging.LogRecord) -> str:
|
|
91
|
-
"""Format log record with emoji and timestamp."""
|
|
92
|
-
flag = self.LEVEL_FLAGS.get(record.levelno, "INFO")
|
|
93
|
-
emoji = _EMOJI_MAP.get(flag, "ℹ️")
|
|
94
|
-
time_str = datetime.now().strftime("%H:%M:%S")
|
|
95
|
-
message = record.getMessage()
|
|
96
|
-
return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
|
|
97
|
-
|
|
98
|
-
# =============================================================================
|
|
99
|
-
# CONFIGURATION
|
|
100
|
-
# =============================================================================
|
|
101
|
-
|
|
102
|
-
def _ensure_basic_config() -> None:
|
|
103
|
-
"""Ensure logging is configured (called once)."""
|
|
104
|
-
global _configured
|
|
105
|
-
if _configured:
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
root_logger = logging.getLogger()
|
|
109
|
-
root_logger.setLevel(logging.INFO)
|
|
110
|
-
|
|
111
|
-
# Remove existing handlers to avoid duplicates
|
|
112
|
-
for handler in root_logger.handlers[:]:
|
|
113
|
-
root_logger.removeHandler(handler)
|
|
114
|
-
|
|
115
|
-
# Add console handler with custom formatter and UTF-8 encoding for Windows
|
|
116
|
-
# Wrap stdout with UTF-8 encoding to handle emoji characters on Windows
|
|
117
|
-
if sys.platform == "win32":
|
|
118
|
-
# On Windows, wrap stdout with UTF-8 encoding
|
|
119
|
-
try:
|
|
120
|
-
# Try to set UTF-8 encoding for stdout
|
|
121
|
-
if hasattr(sys.stdout, 'reconfigure'):
|
|
122
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
123
|
-
# Create a wrapper stream that handles encoding
|
|
124
|
-
utf8_stream = io.TextIOWrapper(
|
|
125
|
-
sys.stdout.buffer,
|
|
126
|
-
encoding='utf-8',
|
|
127
|
-
errors='replace',
|
|
128
|
-
line_buffering=True
|
|
129
|
-
)
|
|
130
|
-
console_handler = logging.StreamHandler(utf8_stream)
|
|
131
|
-
except (AttributeError, OSError):
|
|
132
|
-
# Fallback to regular stdout if reconfiguration fails
|
|
133
|
-
console_handler = logging.StreamHandler(sys.stdout)
|
|
134
|
-
else:
|
|
135
|
-
console_handler = logging.StreamHandler(sys.stdout)
|
|
136
|
-
|
|
137
|
-
console_handler.setLevel(logging.INFO)
|
|
138
|
-
console_handler.setFormatter(XWLazyFormatter())
|
|
139
|
-
root_logger.addHandler(console_handler)
|
|
140
|
-
|
|
141
|
-
# Load environment overrides
|
|
142
|
-
_load_env_overrides()
|
|
143
|
-
|
|
144
|
-
_configured = True
|
|
145
|
-
|
|
146
|
-
# =============================================================================
|
|
147
|
-
# PUBLIC API
|
|
148
|
-
# =============================================================================
|
|
149
|
-
|
|
150
|
-
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
151
|
-
"""
|
|
152
|
-
Return a logger configured for the lazy subsystem.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
name: Optional logger name (defaults to "xwlazy.lazy")
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
Configured logger instance
|
|
159
|
-
"""
|
|
160
|
-
_ensure_basic_config()
|
|
161
|
-
return logging.getLogger(name or "xwlazy.lazy")
|
|
162
|
-
|
|
163
|
-
def is_log_category_enabled(category: str) -> bool:
|
|
164
|
-
"""
|
|
165
|
-
Return True if the provided log category is enabled.
|
|
166
|
-
|
|
167
|
-
Args:
|
|
168
|
-
category: Log category name (e.g., "install", "hook")
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
True if category is enabled, False otherwise
|
|
172
|
-
"""
|
|
173
|
-
_ensure_basic_config()
|
|
174
|
-
normalized = _normalize_category(category)
|
|
175
|
-
if normalized in _category_overrides:
|
|
176
|
-
return _category_overrides[normalized]
|
|
177
|
-
return _CATEGORY_DEFAULTS.get(normalized, True)
|
|
178
|
-
|
|
179
|
-
def set_log_category(category: str, enabled: bool) -> None:
|
|
180
|
-
"""
|
|
181
|
-
Enable/disable an individual log category at runtime.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
category: Log category name
|
|
185
|
-
enabled: True to enable, False to disable
|
|
186
|
-
"""
|
|
187
|
-
_category_overrides[_normalize_category(category)] = bool(enabled)
|
|
188
|
-
|
|
189
|
-
def set_log_categories(overrides: dict[str, bool]) -> None:
|
|
190
|
-
"""
|
|
191
|
-
Bulk update multiple log categories.
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
overrides: Dictionary mapping category names to enabled state
|
|
195
|
-
"""
|
|
196
|
-
for category, enabled in overrides.items():
|
|
197
|
-
set_log_category(category, enabled)
|
|
198
|
-
|
|
199
|
-
def get_log_categories() -> dict[str, bool]:
|
|
200
|
-
"""
|
|
201
|
-
Return the effective state for each built-in log category.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
Dictionary mapping category names to enabled state
|
|
205
|
-
"""
|
|
206
|
-
_ensure_basic_config()
|
|
207
|
-
result = {}
|
|
208
|
-
for category, default_enabled in _CATEGORY_DEFAULTS.items():
|
|
209
|
-
normalized = _normalize_category(category)
|
|
210
|
-
result[category] = _category_overrides.get(normalized, default_enabled)
|
|
211
|
-
return result
|
|
212
|
-
|
|
213
|
-
def log_event(category: str, level_fn, msg: str, *args, **kwargs) -> None:
|
|
214
|
-
"""
|
|
215
|
-
Emit a log for the given category if it is enabled.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
category: Log category name
|
|
219
|
-
level_fn: Logging function (e.g., logger.info, logger.warning)
|
|
220
|
-
msg: Log message format string
|
|
221
|
-
*args: Positional arguments for message formatting
|
|
222
|
-
**kwargs: Keyword arguments for message formatting
|
|
223
|
-
"""
|
|
224
|
-
if is_log_category_enabled(category):
|
|
225
|
-
level_fn(msg, *args, **kwargs)
|
|
226
|
-
|
|
227
|
-
def format_message(flag: str, message: str) -> str:
|
|
228
|
-
"""
|
|
229
|
-
Format a message with exonware.xwlazy [HH:MM:SS]: [FLAG] format.
|
|
230
|
-
|
|
231
|
-
Args:
|
|
232
|
-
flag: Log flag (e.g., "INFO", "WARN", "ERROR")
|
|
233
|
-
message: Message text
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
Formatted message string
|
|
237
|
-
"""
|
|
238
|
-
emoji = _EMOJI_MAP.get(flag, "ℹ️")
|
|
239
|
-
time_str = datetime.now().strftime("%H:%M:%S")
|
|
240
|
-
return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
|
|
241
|
-
|
|
242
|
-
def print_formatted(flag: str, message: str, same_line: bool = False) -> None:
|
|
243
|
-
"""
|
|
244
|
-
Print a formatted message with optional same-line support.
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
flag: Log flag (e.g., "INFO", "WARN", "ERROR")
|
|
248
|
-
message: Message text
|
|
249
|
-
same_line: If True, use carriage return for same-line output
|
|
250
|
-
"""
|
|
251
|
-
formatted = format_message(flag, message)
|
|
252
|
-
if same_line:
|
|
253
|
-
sys.stdout.write(f"\r{formatted}")
|
|
254
|
-
sys.stdout.flush()
|
|
255
|
-
else:
|
|
256
|
-
print(formatted)
|
|
257
|
-
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Common Services
|
|
3
|
-
|
|
4
|
-
Shared services used by both modules and packages.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from .dependency_mapper import DependencyMapper
|
|
8
|
-
from .spec_cache import (
|
|
9
|
-
_spec_cache_get,
|
|
10
|
-
_spec_cache_put,
|
|
11
|
-
_spec_cache_clear,
|
|
12
|
-
_cache_spec_if_missing,
|
|
13
|
-
get_stdlib_module_set,
|
|
14
|
-
)
|
|
15
|
-
from .state_manager import LazyStateManager
|
|
16
|
-
from .keyword_detection import (
|
|
17
|
-
enable_keyword_detection,
|
|
18
|
-
is_keyword_detection_enabled,
|
|
19
|
-
get_keyword_detection_keyword,
|
|
20
|
-
check_package_keywords,
|
|
21
|
-
_detect_lazy_installation,
|
|
22
|
-
_detect_meta_info_mode,
|
|
23
|
-
)
|
|
24
|
-
from .install_cache_utils import (
|
|
25
|
-
get_default_cache_dir,
|
|
26
|
-
get_cache_dir,
|
|
27
|
-
get_wheel_path,
|
|
28
|
-
get_install_tree_dir,
|
|
29
|
-
get_site_packages_dir,
|
|
30
|
-
pip_install_from_path,
|
|
31
|
-
ensure_cached_wheel,
|
|
32
|
-
install_from_cached_tree,
|
|
33
|
-
materialize_cached_tree,
|
|
34
|
-
has_cached_install_tree,
|
|
35
|
-
install_from_cached_wheel,
|
|
36
|
-
)
|
|
37
|
-
from .install_async_utils import (
|
|
38
|
-
get_package_size_mb,
|
|
39
|
-
async_install_package,
|
|
40
|
-
async_uninstall_package,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
__all__ = [
|
|
44
|
-
'DependencyMapper',
|
|
45
|
-
'_spec_cache_get',
|
|
46
|
-
'_spec_cache_put',
|
|
47
|
-
'_spec_cache_clear',
|
|
48
|
-
'_cache_spec_if_missing',
|
|
49
|
-
'get_stdlib_module_set',
|
|
50
|
-
'LazyStateManager',
|
|
51
|
-
'enable_keyword_detection',
|
|
52
|
-
'is_keyword_detection_enabled',
|
|
53
|
-
'get_keyword_detection_keyword',
|
|
54
|
-
'check_package_keywords',
|
|
55
|
-
'_detect_lazy_installation',
|
|
56
|
-
'_detect_meta_info_mode',
|
|
57
|
-
'get_default_cache_dir',
|
|
58
|
-
'get_cache_dir',
|
|
59
|
-
'get_wheel_path',
|
|
60
|
-
'get_install_tree_dir',
|
|
61
|
-
'get_site_packages_dir',
|
|
62
|
-
'pip_install_from_path',
|
|
63
|
-
'ensure_cached_wheel',
|
|
64
|
-
'install_from_cached_tree',
|
|
65
|
-
'materialize_cached_tree',
|
|
66
|
-
'has_cached_install_tree',
|
|
67
|
-
'install_from_cached_wheel',
|
|
68
|
-
'get_package_size_mb',
|
|
69
|
-
'async_install_package',
|
|
70
|
-
'async_uninstall_package',
|
|
71
|
-
]
|
|
72
|
-
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Dependency Mapper for package discovery.
|
|
3
|
-
|
|
4
|
-
This module contains DependencyMapper class extracted from lazy_core.py Section 1.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import threading
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
|
-
# Lazy import to avoid circular dependency
|
|
11
|
-
def _get_logger():
|
|
12
|
-
"""Get logger (lazy import to avoid circular dependency)."""
|
|
13
|
-
from ...common.logger import get_logger
|
|
14
|
-
return get_logger("xwlazy.discovery")
|
|
15
|
-
|
|
16
|
-
logger = None # Will be initialized on first use
|
|
17
|
-
|
|
18
|
-
# Import from spec_cache (same directory)
|
|
19
|
-
from .spec_cache import (
|
|
20
|
-
_cached_stdlib_check,
|
|
21
|
-
_spec_cache_get,
|
|
22
|
-
_cache_spec_if_missing,
|
|
23
|
-
get_stdlib_module_set,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
_STDLIB_MODULE_SET = get_stdlib_module_set()
|
|
27
|
-
|
|
28
|
-
# Import from manifest (package/services - avoid circular import)
|
|
29
|
-
def get_manifest_loader():
|
|
30
|
-
"""Get manifest loader (lazy import to avoid circular dependency)."""
|
|
31
|
-
from ...package.services.manifest import get_manifest_loader as _get_manifest_loader
|
|
32
|
-
return _get_manifest_loader()
|
|
33
|
-
|
|
34
|
-
# Import from discovery (package/services - avoid circular import)
|
|
35
|
-
def get_lazy_discovery():
|
|
36
|
-
"""Get discovery instance."""
|
|
37
|
-
from ...package.services.discovery import get_lazy_discovery as _get_lazy_discovery
|
|
38
|
-
return _get_lazy_discovery()
|
|
39
|
-
|
|
40
|
-
class DependencyMapper:
|
|
41
|
-
"""
|
|
42
|
-
Maps import names to package names using dynamic discovery.
|
|
43
|
-
Optimized with caching to avoid repeated file I/O.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
__slots__ = (
|
|
47
|
-
'_discovery',
|
|
48
|
-
'_package_import_mapping',
|
|
49
|
-
'_import_package_mapping',
|
|
50
|
-
'_cached',
|
|
51
|
-
'_lock',
|
|
52
|
-
'_package_name',
|
|
53
|
-
'_manifest_generation',
|
|
54
|
-
'_manifest_dependencies',
|
|
55
|
-
'_manifest_signature',
|
|
56
|
-
'_manifest_empty',
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def __init__(self, package_name: str = 'default'):
|
|
60
|
-
"""Initialize dependency mapper."""
|
|
61
|
-
self._discovery = None # Lazy init to avoid circular imports
|
|
62
|
-
self._package_import_mapping: dict[str, list[str]] = {}
|
|
63
|
-
self._import_package_mapping: dict[str, str] = {}
|
|
64
|
-
self._cached = False
|
|
65
|
-
self._lock = threading.RLock()
|
|
66
|
-
self._package_name = package_name
|
|
67
|
-
self._manifest_generation = -1
|
|
68
|
-
self._manifest_dependencies: dict[str, str] = {}
|
|
69
|
-
self._manifest_signature: Optional[tuple[str, float, float]] = None
|
|
70
|
-
self._manifest_empty = False
|
|
71
|
-
|
|
72
|
-
def set_package_name(self, package_name: str) -> None:
|
|
73
|
-
"""Update the owning package name (affects manifest lookups)."""
|
|
74
|
-
normalized = (package_name or 'default').strip().lower() or 'default'
|
|
75
|
-
if normalized != self._package_name:
|
|
76
|
-
self._package_name = normalized
|
|
77
|
-
self._manifest_generation = -1
|
|
78
|
-
self._manifest_dependencies = {}
|
|
79
|
-
|
|
80
|
-
def _get_discovery(self):
|
|
81
|
-
"""Get discovery instance (lazy init)."""
|
|
82
|
-
if self._discovery is None:
|
|
83
|
-
self._discovery = get_lazy_discovery()
|
|
84
|
-
return self._discovery
|
|
85
|
-
|
|
86
|
-
def _ensure_mappings_cached(self) -> None:
|
|
87
|
-
"""Ensure mappings are cached (lazy initialization)."""
|
|
88
|
-
if self._cached:
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
with self._lock:
|
|
92
|
-
if self._cached:
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
discovery = self._get_discovery()
|
|
96
|
-
self._package_import_mapping = discovery.get_package_import_mapping()
|
|
97
|
-
self._import_package_mapping = discovery.get_import_package_mapping()
|
|
98
|
-
self._cached = True
|
|
99
|
-
|
|
100
|
-
def _ensure_manifest_cached(self, loader=None) -> None:
|
|
101
|
-
if loader is None:
|
|
102
|
-
loader = get_manifest_loader()
|
|
103
|
-
signature = loader.get_manifest_signature(self._package_name)
|
|
104
|
-
if signature == self._manifest_signature and (self._manifest_dependencies or self._manifest_empty):
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
shared = loader.get_shared_dependencies(self._package_name, signature)
|
|
108
|
-
if shared is not None:
|
|
109
|
-
self._manifest_generation = loader.generation
|
|
110
|
-
self._manifest_signature = signature
|
|
111
|
-
self._manifest_dependencies = shared
|
|
112
|
-
self._manifest_empty = len(shared) == 0
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
manifest = loader.get_manifest(self._package_name)
|
|
116
|
-
current_generation = loader.generation
|
|
117
|
-
|
|
118
|
-
dependencies: dict[str, str] = {}
|
|
119
|
-
manifest_empty = True
|
|
120
|
-
if manifest and manifest.dependencies:
|
|
121
|
-
dependencies = {
|
|
122
|
-
key.lower(): value
|
|
123
|
-
for key, value in manifest.dependencies.items()
|
|
124
|
-
if key and value
|
|
125
|
-
}
|
|
126
|
-
manifest_empty = False
|
|
127
|
-
|
|
128
|
-
self._manifest_generation = current_generation
|
|
129
|
-
self._manifest_signature = signature
|
|
130
|
-
self._manifest_dependencies = dependencies
|
|
131
|
-
self._manifest_empty = manifest_empty
|
|
132
|
-
|
|
133
|
-
@staticmethod
|
|
134
|
-
def _is_stdlib_or_builtin(module_name: str) -> bool:
|
|
135
|
-
"""Return True if the module is built-in or part of the stdlib."""
|
|
136
|
-
root = module_name.split('.', 1)[0]
|
|
137
|
-
needs_cache = False
|
|
138
|
-
if module_name in _STDLIB_MODULE_SET or root in _STDLIB_MODULE_SET:
|
|
139
|
-
return True
|
|
140
|
-
if _cached_stdlib_check(module_name):
|
|
141
|
-
needs_cache = True
|
|
142
|
-
if needs_cache:
|
|
143
|
-
_cache_spec_if_missing(module_name)
|
|
144
|
-
return needs_cache
|
|
145
|
-
|
|
146
|
-
DENY_LIST: set[str] = {
|
|
147
|
-
# POSIX-only modules that don't exist on Windows but try to auto-install
|
|
148
|
-
"pwd",
|
|
149
|
-
"grp",
|
|
150
|
-
"spwd",
|
|
151
|
-
"nis",
|
|
152
|
-
"termios",
|
|
153
|
-
"tty",
|
|
154
|
-
"pty",
|
|
155
|
-
"fcntl",
|
|
156
|
-
# Windows-only internals
|
|
157
|
-
"winreg",
|
|
158
|
-
"winsound",
|
|
159
|
-
"_winapi",
|
|
160
|
-
"_dbm",
|
|
161
|
-
# Internal optional modules that must never trigger auto-install
|
|
162
|
-
"compression",
|
|
163
|
-
"socks",
|
|
164
|
-
"wimlib",
|
|
165
|
-
# Optional dependencies with Python 2 compatibility shims (Python 3.8+ only)
|
|
166
|
-
"inspect2", # Python 2 compatibility shim, not needed on Python 3.8+
|
|
167
|
-
"rich", # Optional CLI enhancement for httpx, not required for core functionality
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
def _should_skip_auto_install(self, import_name: str) -> bool:
|
|
171
|
-
"""Determine whether an import should bypass lazy installation."""
|
|
172
|
-
global logger
|
|
173
|
-
if logger is None:
|
|
174
|
-
logger = _get_logger()
|
|
175
|
-
|
|
176
|
-
if self._is_stdlib_or_builtin(import_name):
|
|
177
|
-
logger.debug("Skipping lazy install for stdlib module '%s'", import_name)
|
|
178
|
-
return True
|
|
179
|
-
|
|
180
|
-
if import_name in self.DENY_LIST:
|
|
181
|
-
logger.debug("Skipping lazy install for denied module '%s'", import_name)
|
|
182
|
-
return True
|
|
183
|
-
|
|
184
|
-
return False
|
|
185
|
-
|
|
186
|
-
def get_package_name(self, import_name: str) -> Optional[str]:
|
|
187
|
-
"""
|
|
188
|
-
Get package name from import name.
|
|
189
|
-
|
|
190
|
-
Priority order (manifest takes precedence):
|
|
191
|
-
1. Skip checks (stdlib, deny list)
|
|
192
|
-
2. Manifest dependencies (explicit user configuration - highest priority)
|
|
193
|
-
3. Spec cache (module already exists - skip auto-install)
|
|
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
|
|
197
|
-
"""
|
|
198
|
-
if self._should_skip_auto_install(import_name):
|
|
199
|
-
return None
|
|
200
|
-
|
|
201
|
-
# Check manifest FIRST - explicit user configuration takes precedence
|
|
202
|
-
loader = get_manifest_loader()
|
|
203
|
-
generation_changed = self._manifest_generation != loader.generation
|
|
204
|
-
manifest_uninitialized = not self._manifest_dependencies and not self._manifest_empty
|
|
205
|
-
if generation_changed or manifest_uninitialized:
|
|
206
|
-
self._ensure_manifest_cached(loader)
|
|
207
|
-
manifest_hit = self._manifest_dependencies.get(import_name.lower())
|
|
208
|
-
if manifest_hit:
|
|
209
|
-
return manifest_hit
|
|
210
|
-
|
|
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
|
|
222
|
-
if _spec_cache_get(import_name):
|
|
223
|
-
return None
|
|
224
|
-
|
|
225
|
-
# Try discovery mappings (from project configs)
|
|
226
|
-
self._ensure_mappings_cached()
|
|
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
|
|
233
|
-
|
|
234
|
-
def get_import_names(self, package_name: str) -> list[str]:
|
|
235
|
-
"""Get all possible import names for a package."""
|
|
236
|
-
self._ensure_mappings_cached()
|
|
237
|
-
return self._package_import_mapping.get(package_name, [package_name])
|
|
238
|
-
|
|
239
|
-
def get_package_import_mapping(self) -> dict[str, list[str]]:
|
|
240
|
-
"""Get complete package to import names mapping."""
|
|
241
|
-
self._ensure_mappings_cached()
|
|
242
|
-
return self._package_import_mapping.copy()
|
|
243
|
-
|
|
244
|
-
def get_import_package_mapping(self) -> dict[str, str]:
|
|
245
|
-
"""Get complete import to package name mapping."""
|
|
246
|
-
self._ensure_mappings_cached()
|
|
247
|
-
return self._import_package_mapping.copy()
|
|
248
|
-
|
|
249
|
-
__all__ = ['DependencyMapper']
|
|
250
|
-
|