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,249 +0,0 @@
1
- """
2
- Configuration management for lazy loading system.
3
-
4
- This module contains LazyInstallConfig which manages per-package lazy installation
5
- configuration. Extracted from lazy_core.py Section 5.
6
- """
7
-
8
- from typing import Optional
9
- from ...common.services import LazyStateManager
10
- from ...defs import LazyLoadMode, LazyInstallMode, LazyModeConfig
11
- from ...defs import get_preset_mode
12
-
13
- # Lazy import to avoid circular dependency
14
- def _get_logger():
15
- """Get logger (lazy import to avoid circular dependency)."""
16
- from ...common.logger import get_logger
17
- return get_logger("xwlazy.config")
18
-
19
- def _get_log_event():
20
- """Get log_event function (lazy import to avoid circular dependency)."""
21
- from ...common.logger import log_event
22
- return log_event
23
-
24
- logger = None # Will be initialized on first use
25
- _log = None # Will be initialized on first use
26
-
27
- # Mode enum mapping - extracted from lazy_core.py
28
- _MODE_ENUM_MAP = {
29
- # Core v1.0 modes
30
- "none": LazyInstallMode.NONE,
31
- "smart": LazyInstallMode.SMART,
32
- "full": LazyInstallMode.FULL,
33
- "clean": LazyInstallMode.CLEAN,
34
- "temporary": LazyInstallMode.TEMPORARY,
35
- "size_aware": LazyInstallMode.SIZE_AWARE,
36
- # Special purpose modes
37
- "interactive": LazyInstallMode.INTERACTIVE,
38
- "warn": LazyInstallMode.WARN,
39
- "disabled": LazyInstallMode.DISABLED,
40
- "dry_run": LazyInstallMode.DRY_RUN,
41
- # Legacy aliases
42
- "auto": LazyInstallMode.SMART,
43
- "on_demand": LazyInstallMode.SMART,
44
- "on-demand": LazyInstallMode.SMART,
45
- "lazy": LazyInstallMode.SMART,
46
- }
47
-
48
- class LazyInstallConfig:
49
- """Global configuration for lazy installation per package."""
50
- _configs: dict[str, bool] = {}
51
- _modes: dict[str, str] = {}
52
- _load_modes: dict[str, LazyLoadMode] = {}
53
- _install_modes: dict[str, LazyInstallMode] = {}
54
- _mode_configs: dict[str, LazyModeConfig] = {}
55
- _initialized: dict[str, bool] = {}
56
- _manual_overrides: dict[str, bool] = {}
57
-
58
- @classmethod
59
- def set(
60
- cls,
61
- package_name: str,
62
- enabled: bool,
63
- mode: str = "auto",
64
- install_hook: bool = True,
65
- manual: bool = False,
66
- load_mode: Optional[LazyLoadMode] = None,
67
- install_mode: Optional[LazyInstallMode] = None,
68
- mode_config: Optional[LazyModeConfig] = None,
69
- ) -> None:
70
- """Enable or disable lazy installation for a specific package."""
71
- package_key = package_name.lower()
72
- state_manager = LazyStateManager(package_name)
73
-
74
- if manual:
75
- cls._manual_overrides[package_key] = True
76
- state_manager.set_manual_state(enabled)
77
- elif cls._manual_overrides.get(package_key):
78
- global logger
79
- if logger is None:
80
- logger = _get_logger()
81
- logger.debug(
82
- f"Lazy install config for {package_key} already overridden manually; skipping auto configuration."
83
- )
84
- return
85
- else:
86
- state_manager.set_manual_state(None)
87
-
88
- cls._configs[package_key] = enabled
89
- cls._modes[package_key] = mode
90
-
91
- # Handle two-dimensional mode configuration
92
- if mode_config:
93
- cls._mode_configs[package_key] = mode_config
94
- cls._load_modes[package_key] = mode_config.load_mode
95
- cls._install_modes[package_key] = mode_config.install_mode
96
- elif load_mode is not None or install_mode is not None:
97
- # Explicit mode specification
98
- if load_mode is None:
99
- load_mode = LazyLoadMode.AUTO # Default
100
- if install_mode is None:
101
- install_mode = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.SMART)
102
- cls._load_modes[package_key] = load_mode
103
- cls._install_modes[package_key] = install_mode
104
- cls._mode_configs[package_key] = LazyModeConfig(
105
- load_mode=load_mode,
106
- install_mode=install_mode
107
- )
108
- else:
109
- # Legacy mode string - try to resolve to preset or default
110
- preset = get_preset_mode(mode)
111
- if preset:
112
- cls._mode_configs[package_key] = preset
113
- cls._load_modes[package_key] = preset.load_mode
114
- cls._install_modes[package_key] = preset.install_mode
115
- else:
116
- # Fallback to legacy behavior
117
- install_mode_enum = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.SMART)
118
- cls._load_modes[package_key] = LazyLoadMode.AUTO
119
- cls._install_modes[package_key] = install_mode_enum
120
- cls._mode_configs[package_key] = LazyModeConfig(
121
- load_mode=LazyLoadMode.AUTO,
122
- install_mode=install_mode_enum
123
- )
124
-
125
- cls._initialize_package(package_key, enabled, mode, install_hook=install_hook)
126
-
127
- @classmethod
128
- def _initialize_package(cls, package_key: str, enabled: bool, mode: str, install_hook: bool = True) -> None:
129
- """Initialize lazy installation for a specific package."""
130
- global logger, _log
131
- if logger is None:
132
- logger = _get_logger()
133
- if _log is None:
134
- _log = _get_log_event()
135
-
136
- # Deferred imports to avoid circular dependency
137
- from .install_registry import LazyInstallerRegistry
138
- from ...facade import (
139
- enable_lazy_install,
140
- disable_lazy_install,
141
- set_lazy_install_mode,
142
- enable_lazy_imports,
143
- install_import_hook,
144
- uninstall_import_hook,
145
- is_import_hook_installed,
146
- sync_manifest_configuration,
147
- )
148
- import asyncio
149
-
150
- if enabled:
151
- try:
152
- # Don't call enable_lazy_install() here - it would create infinite recursion
153
- # The config is already set by LazyInstallConfig.set() above
154
-
155
- # Use explicitly set install_mode from config, or derive from mode string
156
- # Check if install_mode was explicitly set by checking if package_key exists in _install_modes
157
- if package_key in cls._install_modes:
158
- # install_mode was explicitly set in set() method, don't override it
159
- mode_enum = cls._install_modes[package_key]
160
- else:
161
- # Not explicitly set, derive from mode string
162
- mode_enum = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.SMART)
163
- set_lazy_install_mode(package_key, mode_enum)
164
-
165
- # Get load mode from config
166
- load_mode = cls.get_load_mode(package_key)
167
-
168
- # Enable lazy imports with appropriate load mode (skip if NONE mode)
169
- if load_mode != LazyLoadMode.NONE:
170
- enable_lazy_imports(load_mode, package_name=package_key)
171
-
172
- # Enable async for modes that support it
173
- installer = LazyInstallerRegistry.get_instance(package_key)
174
- if installer:
175
- # CRITICAL: Enable the installer (it's disabled by default)
176
- installer.enable()
177
-
178
- if mode_enum in (LazyInstallMode.SMART, LazyInstallMode.FULL, LazyInstallMode.CLEAN, LazyInstallMode.TEMPORARY):
179
- installer._async_enabled = True
180
- installer._ensure_async_loop()
181
-
182
- # For FULL mode, install all dependencies on start
183
- if mode_enum == LazyInstallMode.FULL:
184
- loop = installer._async_loop
185
- if loop:
186
- asyncio.run_coroutine_threadsafe(installer.install_all_dependencies(), loop)
187
-
188
- if install_hook:
189
- if not is_import_hook_installed(package_key):
190
- install_import_hook(package_key)
191
- _log("config", logger.info, f"✅ Lazy installation initialized for {package_key} (install_mode: {mode}, load_mode: {load_mode.value}, hook: installed)")
192
- else:
193
- uninstall_import_hook(package_key)
194
- _log("config", logger.info, f"✅ Lazy installation initialized for {package_key} (install_mode: {mode}, load_mode: {load_mode.value}, hook: disabled)")
195
-
196
- cls._initialized[package_key] = True
197
- sync_manifest_configuration(package_key)
198
- except ImportError as e:
199
- if logger is None:
200
- logger = _get_logger()
201
- logger.warning(f"⚠️ Could not enable lazy install for {package_key}: {e}")
202
- else:
203
- try:
204
- disable_lazy_install(package_key)
205
- except ImportError:
206
- pass
207
- uninstall_import_hook(package_key)
208
- cls._initialized[package_key] = False
209
- _log("config", logger.info, f"❌ Lazy installation disabled for {package_key}")
210
- sync_manifest_configuration(package_key)
211
-
212
- @classmethod
213
- def is_enabled(cls, package_name: str) -> bool:
214
- """Check if lazy installation is enabled for a package."""
215
- return cls._configs.get(package_name.lower(), False)
216
-
217
- @classmethod
218
- def get_mode(cls, package_name: str) -> str:
219
- """Get the lazy installation mode for a package."""
220
- return cls._modes.get(package_name.lower(), "auto")
221
-
222
- @classmethod
223
- def get_mode_config(cls, package_name: str) -> Optional[LazyModeConfig]:
224
- """Get the full mode configuration for a package."""
225
- return cls._mode_configs.get(package_name.lower())
226
-
227
- @classmethod
228
- def get_load_mode(cls, package_name: str) -> LazyLoadMode:
229
- """Get the load mode for a package."""
230
- return cls._load_modes.get(package_name.lower(), LazyLoadMode.NONE)
231
-
232
- @classmethod
233
- def get_install_mode(cls, package_name: str) -> LazyInstallMode:
234
- """Get the install mode for a package."""
235
- return cls._install_modes.get(package_name.lower(), LazyInstallMode.NONE)
236
-
237
- @classmethod
238
- def set_install_mode(cls, package_name: str, mode: LazyInstallMode) -> None:
239
- """Set the install mode for a package."""
240
- package_key = package_name.lower()
241
- cls._install_modes[package_key] = mode
242
- # Update mode config if it exists
243
- if package_key in cls._mode_configs:
244
- mode_config = cls._mode_configs[package_key]
245
- cls._mode_configs[package_key] = LazyModeConfig(
246
- load_mode=mode_config.load_mode,
247
- install_mode=mode
248
- )
249
-
@@ -1,435 +0,0 @@
1
- """
2
- #exonware/xwlazy/src/exonware/xwlazy/discovery/discovery.py
3
-
4
- Package discovery implementation.
5
-
6
- Company: eXonware.com
7
- Author: Eng. Muhammad AlShehri
8
- Email: connect@exonware.com
9
-
10
- Generation Date: 10-Oct-2025
11
-
12
- This module provides LazyDiscovery class that discovers dependencies from
13
- project configuration sources with caching support.
14
- """
15
-
16
- import json
17
- import re
18
- import subprocess
19
- import sys
20
- import threading
21
- from pathlib import Path
22
- from typing import Optional
23
-
24
- from ..base import APackageHelper
25
- from ...defs import DependencyInfo
26
- from ...common.logger import get_logger, log_event as _log
27
-
28
- logger = get_logger("xwlazy.discovery")
29
-
30
- class LazyDiscovery(APackageHelper):
31
- """
32
- Discovers dependencies from project configuration sources.
33
- Implements caching with file modification time checks.
34
- """
35
-
36
- # System/built-in modules that should NEVER be auto-installed
37
- SYSTEM_MODULES_BLACKLIST = {
38
- 'pwd', 'grp', 'spwd', 'crypt', 'nis', 'syslog', 'termios', 'tty', 'pty',
39
- 'fcntl', 'resource', 'msvcrt', 'winreg', 'winsound', '_winapi',
40
- 'rpython', 'rply', 'rnc2rng', '_dbm',
41
- 'sys', 'os', 'io', 'time', 'datetime', 'json', 'csv', 'math',
42
- 'random', 're', 'collections', 'itertools', 'functools', 'operator',
43
- 'pathlib', 'shutil', 'glob', 'tempfile', 'pickle', 'copy', 'types',
44
- 'typing', 'abc', 'enum', 'dataclasses', 'contextlib', 'warnings',
45
- 'logging', 'threading', 'multiprocessing', 'subprocess', 'queue',
46
- 'socket', 'select', 'signal', 'asyncio', 'concurrent', 'email',
47
- 'http', 'urllib', 'xml', 'html', 'sqlite3', 'base64', 'hashlib',
48
- 'hmac', 'secrets', 'ssl', 'binascii', 'struct', 'array', 'weakref',
49
- 'gc', 'inspect', 'traceback', 'atexit', 'codecs', 'locale', 'gettext',
50
- 'argparse', 'optparse', 'configparser', 'fileinput', 'stat', 'platform',
51
- 'unittest', 'doctest', 'pdb', 'profile', 'cProfile', 'timeit', 'trace',
52
- # Internal / optional modules that must never trigger auto-install
53
- 'compression', 'socks', 'wimlib',
54
- }
55
-
56
- # Common import name to package name mappings
57
- # Quick access list: Works without loading project configs
58
- # Includes exonware-common packages for instant resolution
59
- COMMON_MAPPINGS = {
60
- # Image processing
61
- 'cv2': 'opencv-python',
62
- 'PIL': 'Pillow',
63
- 'Pillow': 'Pillow',
64
-
65
- # Data formats
66
- 'yaml': 'PyYAML',
67
- 'bson': 'pymongo',
68
- 'msgpack': 'msgpack', # Exonware common: MessagePack
69
- 'cbor2': 'cbor2', # Exonware common: CBOR
70
- 'cbor': 'cbor2',
71
-
72
- # Machine learning / Data science
73
- 'sklearn': 'scikit-learn',
74
- 'bs4': 'beautifulsoup4',
75
- 'numpy': 'numpy',
76
- 'pandas': 'pandas',
77
- 'matplotlib': 'matplotlib',
78
- 'seaborn': 'seaborn',
79
- 'plotly': 'plotly',
80
- 'scipy': 'scipy',
81
- 'scikit-image': 'scikit-image',
82
-
83
- # Web frameworks
84
- 'django': 'Django',
85
- 'flask': 'Flask',
86
- 'fastapi': 'fastapi',
87
- 'uvicorn': 'uvicorn',
88
-
89
- # Database
90
- 'MySQLdb': 'mysqlclient',
91
- 'psycopg2': 'psycopg2-binary',
92
-
93
- # Serialization (Exonware common)
94
- 'lxml': 'lxml',
95
- 'xml': 'lxml',
96
- 'fastavro': 'fastavro', # Exonware common: Avro
97
- 'avro': 'fastavro',
98
- 'protobuf': 'protobuf', # Exonware common: Protocol Buffers
99
- 'pyarrow': 'pyarrow', # Exonware common: Parquet/Feather
100
- 'parquet': 'pyarrow',
101
- 'feather': 'pyarrow',
102
-
103
- # Utilities
104
- 'dateutil': 'python-dateutil',
105
- 'requests_oauthlib': 'requests-oauthlib',
106
- 'google': 'google-api-python-client',
107
- 'jwt': 'PyJWT',
108
- 'crypto': 'pycrypto',
109
- 'Crypto': 'pycrypto',
110
- 'pytest': 'pytest',
111
- 'black': 'black',
112
- 'isort': 'isort',
113
- 'mypy': 'mypy',
114
- 'psutil': 'psutil',
115
- 'colorama': 'colorama',
116
- 'pytz': 'pytz',
117
- 'aiofiles': 'aiofiles',
118
- 'watchdog': 'watchdog',
119
-
120
- # Image utilities
121
- 'wand': 'Wand',
122
- 'exifread': 'ExifRead',
123
- 'piexif': 'piexif',
124
- 'rawpy': 'rawpy',
125
- 'imageio': 'imageio',
126
-
127
- # OpenCV
128
- 'opencv-python': 'opencv-python',
129
- 'opencv-contrib-python': 'opencv-contrib-python',
130
-
131
- # Observability
132
- 'opentelemetry': 'opentelemetry-api',
133
- 'opentelemetry.trace': 'opentelemetry-api',
134
- 'opentelemetry.sdk': 'opentelemetry-sdk',
135
- }
136
-
137
- # ========================================================================
138
- # Stub Implementations for APackageHelper (Installation methods not used by Discovery)
139
- # ========================================================================
140
-
141
- def install_package(self, package_name: str, module_name: Optional[str] = None) -> bool:
142
- return False
143
-
144
- def _check_security_policy(self, package_name: str) -> tuple[bool, str]:
145
- return (False, "Discovery only")
146
-
147
- def _run_pip_install(self, package_name: str, args: list[str]) -> bool:
148
- return False
149
-
150
- def is_cache_valid(self, key: str) -> bool:
151
- return False
152
-
153
- def _check_importability(self, package_name: str) -> bool:
154
- return False
155
-
156
- def _check_persistent_cache(self, package_name: str) -> bool:
157
- return False
158
-
159
- def _mark_installed_in_persistent_cache(self, package_name: str) -> None:
160
- pass
161
-
162
- def _mark_uninstalled_in_persistent_cache(self, package_name: str) -> None:
163
- pass
164
-
165
- def _run_install(self, *package_names: str) -> None:
166
- pass
167
-
168
- def _run_uninstall(self, *package_names: str) -> None:
169
- pass
170
-
171
- def _discover_from_sources(self) -> None:
172
- """Discover dependencies from all sources."""
173
- self._discover_from_pyproject_toml()
174
- self._discover_from_requirements_txt()
175
- self._discover_from_setup_py()
176
- self._discover_from_custom_config()
177
- self._add_common_mappings() # Add well-known mappings (bson->pymongo, cv2->opencv-python, etc.)
178
-
179
- def _is_cache_valid(self) -> bool:
180
- """Check if cached dependencies are still valid."""
181
- if not self._cache_valid or not self._cached_dependencies:
182
- return False
183
-
184
- config_files = [
185
- self.project_root / 'pyproject.toml',
186
- self.project_root / 'requirements.txt',
187
- self.project_root / 'setup.py',
188
- ]
189
-
190
- for config_file in config_files:
191
- if config_file.exists():
192
- try:
193
- current_mtime = config_file.stat().st_mtime
194
- cached_mtime = self._file_mtimes.get(str(config_file), 0)
195
- if current_mtime > cached_mtime:
196
- return False
197
- except Exception:
198
- return False
199
-
200
- return True
201
-
202
- def _update_file_mtimes(self) -> None:
203
- """Update file modification times for cache validation."""
204
- config_files = [
205
- self.project_root / 'pyproject.toml',
206
- self.project_root / 'requirements.txt',
207
- self.project_root / 'setup.py',
208
- ]
209
- for config_file in config_files:
210
- if config_file.exists():
211
- try:
212
- self._file_mtimes[str(config_file)] = config_file.stat().st_mtime
213
- except Exception:
214
- pass
215
-
216
- def _discover_from_pyproject_toml(self) -> None:
217
- """Discover dependencies from pyproject.toml."""
218
- pyproject_path = self.project_root / 'pyproject.toml'
219
- if not pyproject_path.exists():
220
- return
221
-
222
- try:
223
- try:
224
- import tomllib # Python 3.11+
225
- toml_parser = tomllib # type: ignore[assignment]
226
- except ImportError:
227
- try:
228
- import tomli as tomllib # type: ignore[assignment]
229
- toml_parser = tomllib
230
- except ImportError:
231
- _log(
232
- "discovery",
233
- "TOML parser not available; attempting to lazy-install 'tomli'...",
234
- )
235
- try:
236
- subprocess.run(
237
- [sys.executable, "-m", "pip", "install", "tomli"],
238
- check=False,
239
- capture_output=True,
240
- )
241
- import tomli as tomllib # type: ignore[assignment]
242
- toml_parser = tomllib
243
- except Exception as install_exc:
244
- logger.warning(
245
- "tomli installation failed; skipping pyproject.toml discovery "
246
- f"({install_exc})"
247
- )
248
- return
249
-
250
- with open(pyproject_path, 'rb') as f:
251
- data = toml_parser.load(f)
252
-
253
- dependencies = []
254
- if 'project' in data and 'dependencies' in data['project']:
255
- dependencies.extend(data['project']['dependencies'])
256
-
257
- if 'project' in data and 'optional-dependencies' in data['project']:
258
- for group_name, group_deps in data['project']['optional-dependencies'].items():
259
- dependencies.extend(group_deps)
260
-
261
- if 'build-system' in data and 'requires' in data['build-system']:
262
- dependencies.extend(data['build-system']['requires'])
263
-
264
- for dep in dependencies:
265
- self._parse_dependency_string(dep, 'pyproject.toml')
266
-
267
- self._discovery_sources.append('pyproject.toml')
268
- except Exception as e:
269
- logger.warning(f"Could not parse pyproject.toml: {e}")
270
-
271
- def _discover_from_requirements_txt(self) -> None:
272
- """Discover dependencies from requirements.txt."""
273
- requirements_path = self.project_root / 'requirements.txt'
274
- if not requirements_path.exists():
275
- return
276
-
277
- try:
278
- with open(requirements_path, 'r', encoding='utf-8') as f:
279
- for line in f:
280
- line = line.strip()
281
- if line and not line.startswith('#'):
282
- self._parse_dependency_string(line, 'requirements.txt')
283
-
284
- self._discovery_sources.append('requirements.txt')
285
- except Exception as e:
286
- logger.warning(f"Could not parse requirements.txt: {e}")
287
-
288
- def _discover_from_setup_py(self) -> None:
289
- """Discover dependencies from setup.py."""
290
- setup_path = self.project_root / 'setup.py'
291
- if not setup_path.exists():
292
- return
293
-
294
- try:
295
- with open(setup_path, 'r', encoding='utf-8') as f:
296
- content = f.read()
297
-
298
- install_requires_match = re.search(
299
- r'install_requires\s*=\s*\[(.*?)\]',
300
- content,
301
- re.DOTALL
302
- )
303
- if install_requires_match:
304
- deps_str = install_requires_match.group(1)
305
- deps = re.findall(r'["\']([^"\']+)["\']', deps_str)
306
- for dep in deps:
307
- self._parse_dependency_string(dep, 'setup.py')
308
-
309
- self._discovery_sources.append('setup.py')
310
- except Exception as e:
311
- logger.warning(f"Could not parse setup.py: {e}")
312
-
313
- def _discover_from_custom_config(self) -> None:
314
- """Discover dependencies from custom configuration files."""
315
- config_files = [
316
- 'dependency-mappings.json',
317
- 'lazy-dependencies.json',
318
- 'dependencies.json'
319
- ]
320
-
321
- for config_file in config_files:
322
- config_path = self.project_root / config_file
323
- if config_path.exists():
324
- try:
325
- with open(config_path, 'r', encoding='utf-8') as f:
326
- data = json.load(f)
327
-
328
- if isinstance(data, dict):
329
- for import_name, package_name in data.items():
330
- self.discovered_dependencies[import_name] = DependencyInfo(
331
- import_name=import_name,
332
- package_name=package_name,
333
- source=config_file,
334
- category='custom'
335
- )
336
-
337
- self._discovery_sources.append(config_file)
338
- except Exception as e:
339
- logger.warning(f"Could not parse {config_file}: {e}")
340
-
341
- def _parse_dependency_string(self, dep_str: str, source: str) -> None:
342
- """Parse a dependency string and extract dependency information."""
343
- dep_str = re.sub(r'[>=<!=~]+.*', '', dep_str)
344
- dep_str = re.sub(r'\[.*\]', '', dep_str)
345
- dep_str = dep_str.strip()
346
-
347
- if not dep_str:
348
- return
349
-
350
- import_name = dep_str
351
- package_name = dep_str
352
-
353
- if dep_str in self.COMMON_MAPPINGS:
354
- package_name = self.COMMON_MAPPINGS[dep_str]
355
- elif dep_str in self.COMMON_MAPPINGS.values():
356
- for imp_name, pkg_name in self.COMMON_MAPPINGS.items():
357
- if pkg_name == dep_str:
358
- import_name = imp_name
359
- break
360
-
361
- self.discovered_dependencies[import_name] = DependencyInfo(
362
- import_name=import_name,
363
- package_name=package_name,
364
- source=source,
365
- category='discovered'
366
- )
367
-
368
- def _add_common_mappings(self) -> None:
369
- """Add common mappings that might not be in dependency files."""
370
- for import_name, package_name in self.COMMON_MAPPINGS.items():
371
- if import_name not in self.discovered_dependencies:
372
- self.discovered_dependencies[import_name] = DependencyInfo(
373
- import_name=import_name,
374
- package_name=package_name,
375
- source='common_mappings',
376
- category='common'
377
- )
378
-
379
- def get_package_for_import(self, import_name: str) -> Optional[str]:
380
- """Get package name for a given import name."""
381
- mapping = self.discover_all_dependencies()
382
- return mapping.get(import_name)
383
-
384
- def get_imports_for_package(self, package_name: str) -> list[str]:
385
- """Get all possible import names for a package."""
386
- mapping = self.get_package_import_mapping()
387
- return mapping.get(package_name, [package_name])
388
-
389
- def get_package_import_mapping(self) -> dict[str, list[str]]:
390
- """Get mapping of package names to their possible import names."""
391
- self.discover_all_dependencies()
392
-
393
- package_to_imports = {}
394
- for import_name, dep_info in self.discovered_dependencies.items():
395
- package_name = dep_info.package_name
396
-
397
- if package_name not in package_to_imports:
398
- package_to_imports[package_name] = [package_name]
399
-
400
- if import_name != package_name:
401
- if import_name not in package_to_imports[package_name]:
402
- package_to_imports[package_name].append(import_name)
403
-
404
- return package_to_imports
405
-
406
- def get_import_package_mapping(self) -> dict[str, str]:
407
- """Get mapping of import names to package names."""
408
- self.discover_all_dependencies()
409
- return {import_name: dep_info.package_name for import_name, dep_info in self.discovered_dependencies.items()}
410
-
411
- def export_to_json(self, file_path: str) -> None:
412
- """Export discovered dependencies to JSON file."""
413
- data = {
414
- 'dependencies': {name: info.package_name for name, info in self.discovered_dependencies.items()},
415
- 'sources': self.get_discovery_sources(),
416
- 'total_count': len(self.discovered_dependencies)
417
- }
418
-
419
- with open(file_path, 'w', encoding='utf-8') as f:
420
- json.dump(data, f, indent=2, ensure_ascii=False)
421
-
422
- # Global discovery instance
423
- _discovery: Optional[LazyDiscovery] = None
424
- _discovery_lock = threading.RLock()
425
-
426
- def get_lazy_discovery(project_root: Optional[str] = None) -> LazyDiscovery:
427
- """Get or create global discovery instance."""
428
- global _discovery
429
- with _discovery_lock:
430
- if _discovery is None:
431
- _discovery = LazyDiscovery(project_root)
432
- return _discovery
433
-
434
- __all__ = ['LazyDiscovery', 'get_lazy_discovery']
435
-