exonware-xwlazy 0.1.0.22__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 (90) hide show
  1. exonware/__init__.py +86 -16
  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.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +6 -6
  6. exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
  7. exonware/xwlazy/__init__.py +0 -367
  8. exonware/xwlazy/common/__init__.py +0 -47
  9. exonware/xwlazy/common/base.py +0 -56
  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 -232
  14. exonware/xwlazy/common/services/install_async_utils.py +0 -165
  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/config.py +0 -193
  27. exonware/xwlazy/contracts.py +0 -1396
  28. exonware/xwlazy/defs.py +0 -378
  29. exonware/xwlazy/errors.py +0 -276
  30. exonware/xwlazy/facade.py +0 -991
  31. exonware/xwlazy/module/__init__.py +0 -18
  32. exonware/xwlazy/module/base.py +0 -565
  33. exonware/xwlazy/module/data.py +0 -17
  34. exonware/xwlazy/module/facade.py +0 -246
  35. exonware/xwlazy/module/importer_engine.py +0 -2117
  36. exonware/xwlazy/module/strategies/__init__.py +0 -22
  37. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  38. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  39. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  40. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  41. exonware/xwlazy/package/__init__.py +0 -18
  42. exonware/xwlazy/package/base.py +0 -798
  43. exonware/xwlazy/package/conf.py +0 -324
  44. exonware/xwlazy/package/data.py +0 -17
  45. exonware/xwlazy/package/facade.py +0 -480
  46. exonware/xwlazy/package/services/__init__.py +0 -84
  47. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  48. exonware/xwlazy/package/services/config_manager.py +0 -245
  49. exonware/xwlazy/package/services/discovery.py +0 -370
  50. exonware/xwlazy/package/services/host_packages.py +0 -145
  51. exonware/xwlazy/package/services/install_async.py +0 -277
  52. exonware/xwlazy/package/services/install_cache.py +0 -145
  53. exonware/xwlazy/package/services/install_interactive.py +0 -59
  54. exonware/xwlazy/package/services/install_policy.py +0 -156
  55. exonware/xwlazy/package/services/install_registry.py +0 -54
  56. exonware/xwlazy/package/services/install_result.py +0 -17
  57. exonware/xwlazy/package/services/install_sbom.py +0 -153
  58. exonware/xwlazy/package/services/install_utils.py +0 -79
  59. exonware/xwlazy/package/services/installer_engine.py +0 -406
  60. exonware/xwlazy/package/services/lazy_installer.py +0 -718
  61. exonware/xwlazy/package/services/manifest.py +0 -496
  62. exonware/xwlazy/package/services/strategy_registry.py +0 -186
  63. exonware/xwlazy/package/strategies/__init__.py +0 -57
  64. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  65. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  66. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  67. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  68. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  69. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  70. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  71. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  72. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  73. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  74. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  75. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  76. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  77. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  78. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  79. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  80. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  81. exonware/xwlazy/runtime/__init__.py +0 -18
  82. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  83. exonware/xwlazy/runtime/base.py +0 -274
  84. exonware/xwlazy/runtime/facade.py +0 -94
  85. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  86. exonware/xwlazy/runtime/metrics.py +0 -60
  87. exonware/xwlazy/runtime/performance.py +0 -37
  88. exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
  89. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
  90. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,165 +0,0 @@
1
- """
2
- Installation Async Utilities
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
-
8
- Generation Date: 15-Nov-2025
9
-
10
- Shared utilities for async installation operations.
11
- Used by both execution strategies and LazyInstaller.
12
- """
13
-
14
- import os
15
- import sys
16
- import json
17
- import asyncio
18
- import subprocess
19
- from typing import Optional
20
-
21
- # Lazy imports
22
- def _get_logger():
23
- """Get logger (lazy import to avoid circular dependency)."""
24
- from ..logger import get_logger
25
- return get_logger("xwlazy.install_async_utils")
26
-
27
- logger = None
28
-
29
- def _ensure_logging_initialized():
30
- """Ensure logging utilities are initialized."""
31
- global logger
32
- if logger is None:
33
- logger = _get_logger()
34
-
35
- async def get_package_size_mb(package_name: str) -> Optional[float]:
36
- """
37
- Get package size in MB by checking pip show or download size.
38
-
39
- Args:
40
- package_name: Package name to check
41
-
42
- Returns:
43
- Size in MB or None if cannot determine
44
- """
45
- try:
46
- process = await asyncio.create_subprocess_exec(
47
- sys.executable, '-m', 'pip', 'show', package_name,
48
- stdout=asyncio.subprocess.PIPE,
49
- stderr=asyncio.subprocess.PIPE
50
- )
51
- stdout, _ = await process.communicate()
52
-
53
- if process.returncode == 0:
54
- output = stdout.decode()
55
- for line in output.split('\n'):
56
- if line.startswith('Location:'):
57
- location = line.split(':', 1)[1].strip()
58
- try:
59
- total_size = 0
60
- for dirpath, dirnames, filenames in os.walk(location):
61
- for filename in filenames:
62
- filepath = os.path.join(dirpath, filename)
63
- if os.path.exists(filepath):
64
- total_size += os.path.getsize(filepath)
65
- return total_size / (1024 * 1024)
66
- except Exception:
67
- pass
68
- except Exception:
69
- pass
70
-
71
- # Fallback: Try to get download size from PyPI
72
- try:
73
- import urllib.request
74
- url = f"https://pypi.org/pypi/{package_name}/json"
75
- with urllib.request.urlopen(url, timeout=5) as response:
76
- data = json.loads(response.read())
77
- if 'urls' in data and data['urls']:
78
- latest = data['urls'][0]
79
- if 'size' in latest:
80
- return latest['size'] / (1024 * 1024)
81
- except Exception:
82
- pass
83
-
84
- return None
85
-
86
- async def async_install_package(
87
- package_name: str,
88
- policy_args: Optional[list] = None
89
- ) -> tuple[bool, Optional[str]]:
90
- """
91
- Install a package asynchronously using asyncio subprocess.
92
-
93
- Args:
94
- package_name: Package name to install
95
- policy_args: Optional policy arguments (index URLs, trusted hosts, etc.)
96
-
97
- Returns:
98
- Tuple of (success: bool, error_message: Optional[str])
99
- """
100
- _ensure_logging_initialized()
101
- try:
102
- pip_args = [sys.executable, '-m', 'pip', 'install']
103
- if policy_args:
104
- pip_args.extend(policy_args)
105
- pip_args.append(package_name)
106
-
107
- process = await asyncio.create_subprocess_exec(
108
- *pip_args,
109
- stdout=asyncio.subprocess.PIPE,
110
- stderr=asyncio.subprocess.PIPE
111
- )
112
-
113
- stdout, stderr = await process.communicate()
114
-
115
- if process.returncode == 0:
116
- return True, None
117
- else:
118
- error_msg = stderr.decode() if stderr else "Unknown error"
119
- logger.error(f"Failed to install {package_name}: {error_msg}")
120
- return False, error_msg
121
- except Exception as e:
122
- logger.error(f"Error in async install of {package_name}: {e}")
123
- return False, str(e)
124
-
125
- async def async_uninstall_package(
126
- package_name: str,
127
- quiet: bool = True
128
- ) -> bool:
129
- """
130
- Uninstall a package asynchronously.
131
-
132
- Args:
133
- package_name: Package name to uninstall
134
- quiet: If True, suppress output
135
-
136
- Returns:
137
- True if successful, False otherwise
138
- """
139
- _ensure_logging_initialized()
140
- try:
141
- pip_args = [sys.executable, '-m', 'pip', 'uninstall', '-y', package_name]
142
-
143
- process = await asyncio.create_subprocess_exec(
144
- *pip_args,
145
- stdout=asyncio.subprocess.PIPE if quiet else None,
146
- stderr=asyncio.subprocess.PIPE if quiet else None
147
- )
148
-
149
- await process.communicate()
150
-
151
- if process.returncode == 0:
152
- if not quiet:
153
- logger.info(f"Uninstalled {package_name}")
154
- return True
155
- return False
156
- except Exception as e:
157
- logger.debug(f"Failed to uninstall {package_name}: {e}")
158
- return False
159
-
160
- __all__ = [
161
- 'get_package_size_mb',
162
- 'async_install_package',
163
- 'async_uninstall_package',
164
- ]
165
-
@@ -1,245 +0,0 @@
1
- """
2
- Installation Cache Utilities
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
-
8
- Generation Date: 15-Nov-2025
9
-
10
- Shared utilities for cache management (wheels, install trees).
11
- Used by both execution strategies and LazyInstaller.
12
- """
13
-
14
- import os
15
- import sys
16
- import shutil
17
- import sysconfig
18
- import tempfile
19
- import subprocess
20
- import zipfile
21
- from pathlib import Path
22
- from typing import Optional, List
23
- from contextlib import suppress
24
-
25
- # Lazy imports
26
- def _get_logger():
27
- """Get logger (lazy import to avoid circular dependency)."""
28
- from ..logger import get_logger
29
- return get_logger("xwlazy.install_cache_utils")
30
-
31
- logger = None
32
-
33
- # Environment variables
34
- _DEFAULT_ASYNC_CACHE_DIR = Path(
35
- os.environ.get(
36
- "XWLAZY_ASYNC_CACHE_DIR",
37
- os.path.join(os.path.expanduser("~"), ".xwlazy", "wheel-cache"),
38
- )
39
- )
40
-
41
- def _ensure_logging_initialized():
42
- """Ensure logging utilities are initialized."""
43
- global logger
44
- if logger is None:
45
- logger = _get_logger()
46
-
47
- def get_default_cache_dir() -> Path:
48
- """Get the default cache directory."""
49
- return _DEFAULT_ASYNC_CACHE_DIR
50
-
51
- def get_cache_dir(cache_dir: Optional[Path] = None) -> Path:
52
- """Get cache directory, creating it if necessary."""
53
- if cache_dir is None:
54
- cache_dir = _DEFAULT_ASYNC_CACHE_DIR
55
- path = Path(cache_dir).expanduser()
56
- path.mkdir(parents=True, exist_ok=True)
57
- return path
58
-
59
- def get_wheel_path(package_name: str, cache_dir: Optional[Path] = None) -> Path:
60
- """Get the cached wheel file path for a package."""
61
- cache = get_cache_dir(cache_dir)
62
- safe = package_name.replace("/", "_").replace("\\", "_").replace(":", "_")
63
- return cache / f"{safe}.whl"
64
-
65
- def get_install_tree_dir(package_name: str, cache_dir: Optional[Path] = None) -> Path:
66
- """Get the cached install directory for a package."""
67
- cache = get_cache_dir(cache_dir)
68
- safe = package_name.replace("/", "_").replace("\\", "_").replace(":", "_")
69
- return cache / "installs" / safe
70
-
71
- def get_site_packages_dir() -> Path:
72
- """Get the site-packages directory."""
73
- purelib = sysconfig.get_paths().get("purelib")
74
- if not purelib:
75
- purelib = sysconfig.get_paths().get("platlib", sys.prefix)
76
- path = Path(purelib)
77
- path.mkdir(parents=True, exist_ok=True)
78
- return path
79
-
80
- def pip_install_from_path(wheel_path: Path, policy_args: Optional[List[str]] = None) -> bool:
81
- """Install a wheel file using pip."""
82
- try:
83
- pip_args = [
84
- sys.executable,
85
- '-m',
86
- 'pip',
87
- 'install',
88
- '--no-deps',
89
- '--no-input',
90
- '--disable-pip-version-check',
91
- ]
92
- if policy_args:
93
- pip_args.extend(policy_args)
94
- pip_args.append(str(wheel_path))
95
- result = subprocess.run(
96
- pip_args,
97
- capture_output=True,
98
- text=True,
99
- check=True,
100
- )
101
- return result.returncode == 0
102
- except subprocess.CalledProcessError:
103
- return False
104
-
105
- def ensure_cached_wheel(
106
- package_name: str,
107
- policy_args: Optional[List[str]] = None,
108
- cache_dir: Optional[Path] = None
109
- ) -> Optional[Path]:
110
- """Ensure a wheel is cached, downloading it if necessary."""
111
- wheel_path = get_wheel_path(package_name, cache_dir)
112
- if wheel_path.exists():
113
- return wheel_path
114
-
115
- cache = get_cache_dir(cache_dir)
116
- try:
117
- pip_args = [
118
- sys.executable,
119
- '-m',
120
- 'pip',
121
- 'wheel',
122
- '--no-deps',
123
- '--disable-pip-version-check',
124
- ]
125
- if policy_args:
126
- pip_args.extend(policy_args)
127
- pip_args.extend(['--wheel-dir', str(cache), package_name])
128
- result = subprocess.run(
129
- pip_args,
130
- capture_output=True,
131
- text=True,
132
- check=True,
133
- )
134
- if result.returncode != 0:
135
- return None
136
- candidates = sorted(cache.glob("*.whl"), key=lambda p: p.stat().st_mtime, reverse=True)
137
- if not candidates:
138
- return None
139
- primary = candidates[0]
140
- if wheel_path.exists():
141
- with suppress(Exception):
142
- wheel_path.unlink()
143
- primary.rename(wheel_path)
144
- for leftover in candidates[1:]:
145
- with suppress(Exception):
146
- leftover.unlink()
147
- return wheel_path
148
- except subprocess.CalledProcessError:
149
- return None
150
-
151
- def install_from_cached_tree(
152
- package_name: str,
153
- cache_dir: Optional[Path] = None
154
- ) -> bool:
155
- """Install from a cached install tree."""
156
- _ensure_logging_initialized()
157
- src = get_install_tree_dir(package_name, cache_dir)
158
- if not src.exists() or not any(src.iterdir()):
159
- return False
160
- target_root = get_site_packages_dir()
161
- try:
162
- for item in src.iterdir():
163
- dest = target_root / item.name
164
- if dest.exists():
165
- if dest.is_dir():
166
- shutil.rmtree(dest, ignore_errors=True)
167
- else:
168
- with suppress(FileNotFoundError):
169
- dest.unlink()
170
- if item.is_dir():
171
- shutil.copytree(item, dest)
172
- else:
173
- dest.parent.mkdir(parents=True, exist_ok=True)
174
- shutil.copy2(item, dest)
175
- return True
176
- except Exception as exc:
177
- logger.debug("Cached tree install failed for %s: %s", package_name, exc)
178
- return False
179
-
180
- def materialize_cached_tree(
181
- package_name: str,
182
- wheel_path: Path,
183
- cache_dir: Optional[Path] = None
184
- ) -> None:
185
- """Materialize a cached install tree from a wheel file."""
186
- _ensure_logging_initialized()
187
- if not wheel_path or not wheel_path.exists():
188
- return
189
- target_dir = get_install_tree_dir(package_name, cache_dir)
190
- if target_dir.exists() and any(target_dir.iterdir()):
191
- return
192
- parent = target_dir.parent
193
- parent.mkdir(parents=True, exist_ok=True)
194
- temp_dir = Path(
195
- tempfile.mkdtemp(prefix="xwlazy-cache-", dir=str(parent))
196
- )
197
- try:
198
- with zipfile.ZipFile(wheel_path, "r") as archive:
199
- archive.extractall(temp_dir)
200
- if target_dir.exists():
201
- shutil.rmtree(target_dir, ignore_errors=True)
202
- shutil.move(str(temp_dir), str(target_dir))
203
- except Exception as exc:
204
- logger.debug("Failed to materialize cached tree for %s: %s", package_name, exc)
205
- with suppress(Exception):
206
- shutil.rmtree(temp_dir, ignore_errors=True)
207
- else:
208
- return
209
- finally:
210
- if temp_dir.exists():
211
- shutil.rmtree(temp_dir, ignore_errors=True)
212
-
213
- def has_cached_install_tree(
214
- package_name: str,
215
- cache_dir: Optional[Path] = None
216
- ) -> bool:
217
- """Check if a cached install tree exists."""
218
- target = get_install_tree_dir(package_name, cache_dir)
219
- return target.exists() and any(target.iterdir())
220
-
221
- def install_from_cached_wheel(
222
- package_name: str,
223
- policy_args: Optional[List[str]] = None,
224
- cache_dir: Optional[Path] = None
225
- ) -> bool:
226
- """Install from a cached wheel file."""
227
- wheel_path = get_wheel_path(package_name, cache_dir)
228
- if not wheel_path.exists():
229
- return False
230
- return pip_install_from_path(wheel_path, policy_args)
231
-
232
- __all__ = [
233
- 'get_default_cache_dir',
234
- 'get_cache_dir',
235
- 'get_wheel_path',
236
- 'get_install_tree_dir',
237
- 'get_site_packages_dir',
238
- 'pip_install_from_path',
239
- 'ensure_cached_wheel',
240
- 'install_from_cached_tree',
241
- 'materialize_cached_tree',
242
- 'has_cached_install_tree',
243
- 'install_from_cached_wheel',
244
- ]
245
-
@@ -1,283 +0,0 @@
1
- """
2
- Keyword-based detection for lazy installation.
3
-
4
- This module provides functionality to detect packages that opt-in to lazy loading
5
- by including specific keywords in their metadata.
6
- """
7
-
8
- import os
9
- import sys
10
- import threading
11
- from typing import Optional
12
-
13
- from ..logger import get_logger
14
-
15
- logger = get_logger("xwlazy.discovery.keyword")
16
-
17
- # Global configuration
18
- _KEYWORD_DETECTION_ENABLED: bool = True
19
- _KEYWORD_TO_CHECK: str = "xwlazy-enabled"
20
- _keyword_config_lock = threading.RLock()
21
-
22
- # Detection cache
23
- _lazy_detection_cache: dict[str, bool] = {}
24
- _lazy_detection_lock = threading.RLock()
25
-
26
- def _check_package_keywords(package_name: Optional[str] = None, keyword: Optional[str] = None) -> bool:
27
- """
28
- Check if any installed package has the specified keyword in its metadata.
29
-
30
- This allows packages to opt-in to lazy loading by adding:
31
- [project]
32
- keywords = ["xwlazy-enabled"]
33
-
34
- in their pyproject.toml file. The keyword is stored in the package's
35
- metadata when installed.
36
-
37
- Args:
38
- package_name: The package name to check (or None to check all packages)
39
- keyword: The keyword to look for (default: uses _KEYWORD_TO_CHECK)
40
-
41
- Returns:
42
- True if the keyword is found in any relevant package's metadata
43
- """
44
- if not _KEYWORD_DETECTION_ENABLED:
45
- return False
46
-
47
- if sys.version_info < (3, 8):
48
- return False
49
-
50
- try:
51
- from importlib import metadata
52
- except Exception as exc:
53
- logger.debug(f"importlib.metadata unavailable for keyword detection: {exc}")
54
- return False
55
-
56
- with _keyword_config_lock:
57
- search_keyword = (keyword or _KEYWORD_TO_CHECK).lower()
58
-
59
- try:
60
- if package_name:
61
- # Check specific package
62
- try:
63
- dist = metadata.distribution(package_name)
64
- keywords = dist.metadata.get_all('Keywords', [])
65
- if keywords:
66
- # Keywords can be a single string or list
67
- all_keywords = []
68
- for kw in keywords:
69
- if isinstance(kw, str):
70
- # Split comma-separated keywords
71
- all_keywords.extend(k.strip().lower() for k in kw.split(','))
72
- else:
73
- all_keywords.append(str(kw).lower())
74
-
75
- if search_keyword in all_keywords:
76
- logger.info(f"✅ Detected '{search_keyword}' keyword in package: {package_name}")
77
- return True
78
- except metadata.PackageNotFoundError:
79
- return False
80
- else:
81
- # Check all installed packages
82
- for dist in metadata.distributions():
83
- try:
84
- keywords = dist.metadata.get_all('Keywords', [])
85
- if keywords:
86
- all_keywords = []
87
- for kw in keywords:
88
- if isinstance(kw, str):
89
- all_keywords.extend(k.strip().lower() for k in kw.split(','))
90
- else:
91
- all_keywords.append(str(kw).lower())
92
-
93
- if search_keyword in all_keywords:
94
- package_found = dist.metadata.get('Name', 'unknown')
95
- logger.info(f"✅ Detected '{search_keyword}' keyword in package: {package_found}")
96
- return True
97
- except Exception:
98
- continue
99
- except Exception as exc:
100
- logger.debug(f"Failed to check package keywords: {exc}")
101
-
102
- return False
103
-
104
- def _lazy_marker_installed() -> bool:
105
- """Check if the exonware-xwlazy marker package is installed."""
106
- if sys.version_info < (3, 8):
107
- return False
108
-
109
- try:
110
- from importlib import metadata
111
- except Exception as exc:
112
- logger.debug(f"importlib.metadata unavailable for lazy detection: {exc}")
113
- return False
114
-
115
- try:
116
- metadata.distribution("exonware-xwlazy")
117
- logger.info("✅ Detected exonware-xwlazy marker package")
118
- return True
119
- except metadata.PackageNotFoundError:
120
- logger.debug("❌ exonware-xwlazy marker package not installed")
121
- return False
122
-
123
- def _lazy_env_override(package_name: str) -> Optional[bool]:
124
- """Check environment variable override for lazy installation."""
125
- env_var = f"{package_name.upper()}_LAZY_INSTALL"
126
- raw_value = os.environ.get(env_var)
127
- if raw_value is None:
128
- return None
129
-
130
- normalized = raw_value.strip().lower()
131
- if normalized in ("true", "1", "yes", "on"):
132
- return True
133
- if normalized in ("false", "0", "no", "off"):
134
- return False
135
- return None
136
-
137
- def _detect_meta_info_mode(package_name: str) -> Optional[str]:
138
- """
139
- Detect lazy mode from package metadata keywords.
140
-
141
- Checks for keywords like:
142
- - xwlazy-load-install-uninstall (clean mode)
143
- - xwlazy-lite (lite mode)
144
- - xwlazy-smart (smart mode)
145
- - xwlazy-full (full mode)
146
- - xwlazy-auto (auto mode)
147
-
148
- Returns:
149
- Mode string or None if not found
150
- """
151
- try:
152
- import importlib.metadata
153
- try:
154
- dist = importlib.metadata.distribution(package_name)
155
- keywords = dist.metadata.get_all("Keywords", [])
156
- if not keywords:
157
- return None
158
-
159
- keyword_str = " ".join(keywords).lower()
160
-
161
- if "xwlazy-load-install-uninstall" in keyword_str:
162
- return "clean"
163
- if "xwlazy-lite" in keyword_str:
164
- return "lite"
165
- if "xwlazy-smart" in keyword_str:
166
- return "smart"
167
- if "xwlazy-full" in keyword_str:
168
- return "full"
169
- if "xwlazy-auto" in keyword_str:
170
- return "auto"
171
- except importlib.metadata.PackageNotFoundError:
172
- return None
173
- except Exception:
174
- pass
175
- return None
176
-
177
- def enable_keyword_detection(enabled: bool = True, keyword: Optional[str] = None) -> None:
178
- """
179
- Enable/disable keyword-based auto-detection of lazy loading.
180
-
181
- When enabled, xwlazy will check installed packages for a keyword
182
- (default: "xwlazy-enabled") in their metadata. Packages can opt-in
183
- by adding the keyword to their pyproject.toml:
184
-
185
- [project]
186
- keywords = ["xwlazy-enabled"]
187
-
188
- Args:
189
- enabled: Whether to enable keyword detection (default: True)
190
- keyword: Custom keyword to check (default: "xwlazy-enabled")
191
- """
192
- global _KEYWORD_DETECTION_ENABLED, _KEYWORD_TO_CHECK
193
- with _keyword_config_lock:
194
- _KEYWORD_DETECTION_ENABLED = enabled
195
- if keyword is not None:
196
- _KEYWORD_TO_CHECK = keyword
197
- # Clear cache to force re-detection
198
- with _lazy_detection_lock:
199
- _lazy_detection_cache.clear()
200
-
201
- def is_keyword_detection_enabled() -> bool:
202
- """Return whether keyword-based detection is enabled."""
203
- with _keyword_config_lock:
204
- return _KEYWORD_DETECTION_ENABLED
205
-
206
- def get_keyword_detection_keyword() -> str:
207
- """Get the keyword currently being checked for auto-detection."""
208
- with _keyword_config_lock:
209
- return _KEYWORD_TO_CHECK
210
-
211
- def check_package_keywords(package_name: Optional[str] = None, keyword: Optional[str] = None) -> bool:
212
- """
213
- Check if a package (or any package) has the specified keyword in its metadata.
214
-
215
- This is the public API for the keyword detection functionality.
216
-
217
- Args:
218
- package_name: The package name to check (or None to check all packages)
219
- keyword: The keyword to look for (default: uses configured keyword)
220
-
221
- Returns:
222
- True if the keyword is found in the package's metadata
223
- """
224
- return _check_package_keywords(package_name, keyword)
225
-
226
- def _detect_lazy_installation(package_name: str) -> bool:
227
- """
228
- Detect if lazy installation should be enabled for a package.
229
-
230
- This function checks multiple sources in order:
231
- 1. Environment variable override
232
- 2. Manual state (from state manager)
233
- 3. Cached auto state
234
- 4. Marker package detection
235
- 5. Keyword detection
236
-
237
- Args:
238
- package_name: The package name to check
239
-
240
- Returns:
241
- True if lazy installation should be enabled
242
- """
243
- with _lazy_detection_lock:
244
- cached = _lazy_detection_cache.get(package_name)
245
- if cached is not None:
246
- return cached
247
-
248
- env_override = _lazy_env_override(package_name)
249
- if env_override is not None:
250
- with _lazy_detection_lock:
251
- _lazy_detection_cache[package_name] = env_override
252
- return env_override
253
-
254
- from .state_manager import LazyStateManager
255
- state_manager = LazyStateManager(package_name)
256
- manual_state = state_manager.get_manual_state()
257
- if manual_state is not None:
258
- with _lazy_detection_lock:
259
- _lazy_detection_cache[package_name] = manual_state
260
- return manual_state
261
-
262
- cached_state = state_manager.get_cached_auto_state()
263
- if cached_state is not None:
264
- with _lazy_detection_lock:
265
- _lazy_detection_cache[package_name] = cached_state
266
- return cached_state
267
-
268
- # Check marker package first (existing behavior)
269
- marker_detected = _lazy_marker_installed()
270
-
271
- # Also check for keyword in package metadata (new feature)
272
- keyword_detected = _check_package_keywords(package_name)
273
-
274
- # Enable if either marker package OR keyword is found
275
- detected = marker_detected or keyword_detected
276
-
277
- state_manager.set_auto_state(detected)
278
-
279
- with _lazy_detection_lock:
280
- _lazy_detection_cache[package_name] = detected
281
-
282
- return detected
283
-