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.
Files changed (96) hide show
  1. exonware/__init__.py +85 -34
  2. exonware/xwlazy/version.py +5 -5
  3. exonware/xwlazy.py +2546 -0
  4. exonware/xwlazy_external_libs.toml +716 -0
  5. {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +5 -1
  6. exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
  7. exonware/xwlazy/__init__.py +0 -379
  8. exonware/xwlazy/common/__init__.py +0 -55
  9. exonware/xwlazy/common/base.py +0 -65
  10. exonware/xwlazy/common/cache.py +0 -504
  11. exonware/xwlazy/common/logger.py +0 -257
  12. exonware/xwlazy/common/services/__init__.py +0 -72
  13. exonware/xwlazy/common/services/dependency_mapper.py +0 -250
  14. exonware/xwlazy/common/services/install_async_utils.py +0 -170
  15. exonware/xwlazy/common/services/install_cache_utils.py +0 -245
  16. exonware/xwlazy/common/services/keyword_detection.py +0 -283
  17. exonware/xwlazy/common/services/spec_cache.py +0 -165
  18. exonware/xwlazy/common/services/state_manager.py +0 -84
  19. exonware/xwlazy/common/strategies/__init__.py +0 -28
  20. exonware/xwlazy/common/strategies/caching_dict.py +0 -44
  21. exonware/xwlazy/common/strategies/caching_installation.py +0 -88
  22. exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
  23. exonware/xwlazy/common/strategies/caching_lru.py +0 -63
  24. exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
  25. exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
  26. exonware/xwlazy/common/utils.py +0 -142
  27. exonware/xwlazy/config.py +0 -193
  28. exonware/xwlazy/contracts.py +0 -1533
  29. exonware/xwlazy/defs.py +0 -378
  30. exonware/xwlazy/errors.py +0 -276
  31. exonware/xwlazy/facade.py +0 -1137
  32. exonware/xwlazy/host/__init__.py +0 -8
  33. exonware/xwlazy/host/conf.py +0 -16
  34. exonware/xwlazy/module/__init__.py +0 -18
  35. exonware/xwlazy/module/base.py +0 -622
  36. exonware/xwlazy/module/data.py +0 -17
  37. exonware/xwlazy/module/facade.py +0 -246
  38. exonware/xwlazy/module/importer_engine.py +0 -2964
  39. exonware/xwlazy/module/partial_module_detector.py +0 -275
  40. exonware/xwlazy/module/strategies/__init__.py +0 -22
  41. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  42. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  43. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  44. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  45. exonware/xwlazy/package/__init__.py +0 -18
  46. exonware/xwlazy/package/base.py +0 -863
  47. exonware/xwlazy/package/conf.py +0 -324
  48. exonware/xwlazy/package/data.py +0 -17
  49. exonware/xwlazy/package/facade.py +0 -480
  50. exonware/xwlazy/package/services/__init__.py +0 -84
  51. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  52. exonware/xwlazy/package/services/config_manager.py +0 -249
  53. exonware/xwlazy/package/services/discovery.py +0 -435
  54. exonware/xwlazy/package/services/host_packages.py +0 -180
  55. exonware/xwlazy/package/services/install_async.py +0 -291
  56. exonware/xwlazy/package/services/install_cache.py +0 -145
  57. exonware/xwlazy/package/services/install_interactive.py +0 -59
  58. exonware/xwlazy/package/services/install_policy.py +0 -156
  59. exonware/xwlazy/package/services/install_registry.py +0 -54
  60. exonware/xwlazy/package/services/install_result.py +0 -17
  61. exonware/xwlazy/package/services/install_sbom.py +0 -153
  62. exonware/xwlazy/package/services/install_utils.py +0 -79
  63. exonware/xwlazy/package/services/installer_engine.py +0 -406
  64. exonware/xwlazy/package/services/lazy_installer.py +0 -803
  65. exonware/xwlazy/package/services/manifest.py +0 -503
  66. exonware/xwlazy/package/services/strategy_registry.py +0 -324
  67. exonware/xwlazy/package/strategies/__init__.py +0 -57
  68. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  69. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  70. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  71. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  72. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  73. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  74. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  75. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  76. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  77. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  78. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  79. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  80. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  81. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  82. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  83. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  84. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  85. exonware/xwlazy/runtime/__init__.py +0 -18
  86. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  87. exonware/xwlazy/runtime/base.py +0 -274
  88. exonware/xwlazy/runtime/facade.py +0 -94
  89. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  90. exonware/xwlazy/runtime/metrics.py +0 -60
  91. exonware/xwlazy/runtime/performance.py +0 -37
  92. exonware_xwlazy-0.1.0.23.dist-info/RECORD +0 -93
  93. xwlazy/__init__.py +0 -14
  94. xwlazy/lazy.py +0 -30
  95. {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -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
-