exonware-xwlazy 0.1.0.1__py3-none-any.whl → 0.1.0.8__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 (95) hide show
  1. exonware/xwlazy/__init__.py +0 -0
  2. exonware/xwlazy/version.py +2 -2
  3. exonware_xwlazy-0.1.0.8.dist-info/METADATA +0 -0
  4. exonware_xwlazy-0.1.0.8.dist-info/RECORD +6 -0
  5. exonware/__init__.py +0 -42
  6. exonware/xwlazy/common/__init__.py +0 -55
  7. exonware/xwlazy/common/base.py +0 -65
  8. exonware/xwlazy/common/cache.py +0 -504
  9. exonware/xwlazy/common/logger.py +0 -257
  10. exonware/xwlazy/common/services/__init__.py +0 -72
  11. exonware/xwlazy/common/services/dependency_mapper.py +0 -250
  12. exonware/xwlazy/common/services/install_async_utils.py +0 -170
  13. exonware/xwlazy/common/services/install_cache_utils.py +0 -245
  14. exonware/xwlazy/common/services/keyword_detection.py +0 -283
  15. exonware/xwlazy/common/services/spec_cache.py +0 -165
  16. exonware/xwlazy/common/services/state_manager.py +0 -84
  17. exonware/xwlazy/common/strategies/__init__.py +0 -28
  18. exonware/xwlazy/common/strategies/caching_dict.py +0 -44
  19. exonware/xwlazy/common/strategies/caching_installation.py +0 -88
  20. exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
  21. exonware/xwlazy/common/strategies/caching_lru.py +0 -63
  22. exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
  23. exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
  24. exonware/xwlazy/common/utils.py +0 -142
  25. exonware/xwlazy/config.py +0 -193
  26. exonware/xwlazy/contracts.py +0 -1533
  27. exonware/xwlazy/defs.py +0 -378
  28. exonware/xwlazy/errors.py +0 -276
  29. exonware/xwlazy/facade.py +0 -1137
  30. exonware/xwlazy/host/__init__.py +0 -8
  31. exonware/xwlazy/host/conf.py +0 -16
  32. exonware/xwlazy/module/__init__.py +0 -18
  33. exonware/xwlazy/module/base.py +0 -643
  34. exonware/xwlazy/module/data.py +0 -17
  35. exonware/xwlazy/module/facade.py +0 -246
  36. exonware/xwlazy/module/importer_engine.py +0 -2964
  37. exonware/xwlazy/module/partial_module_detector.py +0 -275
  38. exonware/xwlazy/module/strategies/__init__.py +0 -22
  39. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  40. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  41. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  42. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  43. exonware/xwlazy/package/__init__.py +0 -18
  44. exonware/xwlazy/package/base.py +0 -877
  45. exonware/xwlazy/package/conf.py +0 -324
  46. exonware/xwlazy/package/data.py +0 -17
  47. exonware/xwlazy/package/facade.py +0 -480
  48. exonware/xwlazy/package/services/__init__.py +0 -84
  49. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  50. exonware/xwlazy/package/services/config_manager.py +0 -249
  51. exonware/xwlazy/package/services/discovery.py +0 -435
  52. exonware/xwlazy/package/services/host_packages.py +0 -180
  53. exonware/xwlazy/package/services/install_async.py +0 -291
  54. exonware/xwlazy/package/services/install_cache.py +0 -145
  55. exonware/xwlazy/package/services/install_interactive.py +0 -59
  56. exonware/xwlazy/package/services/install_policy.py +0 -156
  57. exonware/xwlazy/package/services/install_registry.py +0 -54
  58. exonware/xwlazy/package/services/install_result.py +0 -17
  59. exonware/xwlazy/package/services/install_sbom.py +0 -153
  60. exonware/xwlazy/package/services/install_utils.py +0 -79
  61. exonware/xwlazy/package/services/installer_engine.py +0 -406
  62. exonware/xwlazy/package/services/lazy_installer.py +0 -803
  63. exonware/xwlazy/package/services/manifest.py +0 -503
  64. exonware/xwlazy/package/services/strategy_registry.py +0 -324
  65. exonware/xwlazy/package/strategies/__init__.py +0 -57
  66. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  67. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  68. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  69. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  70. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  71. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  72. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  73. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  74. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  75. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  76. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  77. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  78. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  79. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  80. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  81. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  82. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  83. exonware/xwlazy/runtime/__init__.py +0 -18
  84. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  85. exonware/xwlazy/runtime/base.py +0 -274
  86. exonware/xwlazy/runtime/facade.py +0 -94
  87. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  88. exonware/xwlazy/runtime/metrics.py +0 -60
  89. exonware/xwlazy/runtime/performance.py +0 -37
  90. exonware_xwlazy-0.1.0.1.dist-info/METADATA +0 -454
  91. exonware_xwlazy-0.1.0.1.dist-info/RECORD +0 -93
  92. xwlazy/__init__.py +0 -14
  93. xwlazy/lazy.py +0 -30
  94. {exonware_xwlazy-0.1.0.1.dist-info → exonware_xwlazy-0.1.0.8.dist-info}/WHEEL +0 -0
  95. {exonware_xwlazy-0.1.0.1.dist-info → exonware_xwlazy-0.1.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,170 +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
- # Fallback if get_logger returns None (should not happen but safety first)
35
- if logger is None:
36
- import logging
37
- logger = logging.getLogger("xwlazy.install_async_utils.fallback")
38
- logger.addHandler(logging.NullHandler())
39
-
40
- async def get_package_size_mb(package_name: str) -> Optional[float]:
41
- """
42
- Get package size in MB by checking pip show or download size.
43
-
44
- Args:
45
- package_name: Package name to check
46
-
47
- Returns:
48
- Size in MB or None if cannot determine
49
- """
50
- try:
51
- process = await asyncio.create_subprocess_exec(
52
- sys.executable, '-m', 'pip', 'show', package_name,
53
- stdout=asyncio.subprocess.PIPE,
54
- stderr=asyncio.subprocess.PIPE
55
- )
56
- stdout, _ = await process.communicate()
57
-
58
- if process.returncode == 0:
59
- output = stdout.decode()
60
- for line in output.split('\n'):
61
- if line.startswith('Location:'):
62
- location = line.split(':', 1)[1].strip()
63
- try:
64
- total_size = 0
65
- for dirpath, dirnames, filenames in os.walk(location):
66
- for filename in filenames:
67
- filepath = os.path.join(dirpath, filename)
68
- if os.path.exists(filepath):
69
- total_size += os.path.getsize(filepath)
70
- return total_size / (1024 * 1024)
71
- except Exception:
72
- pass
73
- except Exception:
74
- pass
75
-
76
- # Fallback: Try to get download size from PyPI
77
- try:
78
- import urllib.request
79
- url = f"https://pypi.org/pypi/{package_name}/json"
80
- with urllib.request.urlopen(url, timeout=5) as response:
81
- data = json.loads(response.read())
82
- if 'urls' in data and data['urls']:
83
- latest = data['urls'][0]
84
- if 'size' in latest:
85
- return latest['size'] / (1024 * 1024)
86
- except Exception:
87
- pass
88
-
89
- return None
90
-
91
- async def async_install_package(
92
- package_name: str,
93
- policy_args: Optional[list] = None
94
- ) -> tuple[bool, Optional[str]]:
95
- """
96
- Install a package asynchronously using asyncio subprocess.
97
-
98
- Args:
99
- package_name: Package name to install
100
- policy_args: Optional policy arguments (index URLs, trusted hosts, etc.)
101
-
102
- Returns:
103
- Tuple of (success: bool, error_message: Optional[str])
104
- """
105
- _ensure_logging_initialized()
106
- try:
107
- pip_args = [sys.executable, '-m', 'pip', 'install']
108
- if policy_args:
109
- pip_args.extend(policy_args)
110
- pip_args.append(package_name)
111
-
112
- process = await asyncio.create_subprocess_exec(
113
- *pip_args,
114
- stdout=asyncio.subprocess.PIPE,
115
- stderr=asyncio.subprocess.PIPE
116
- )
117
-
118
- stdout, stderr = await process.communicate()
119
-
120
- if process.returncode == 0:
121
- return True, None
122
- else:
123
- error_msg = stderr.decode() if stderr else "Unknown error"
124
- logger.error(f"Failed to install {package_name}: {error_msg}")
125
- return False, error_msg
126
- except Exception as e:
127
- logger.error(f"Error in async install of {package_name}: {e}")
128
- return False, str(e)
129
-
130
- async def async_uninstall_package(
131
- package_name: str,
132
- quiet: bool = True
133
- ) -> bool:
134
- """
135
- Uninstall a package asynchronously.
136
-
137
- Args:
138
- package_name: Package name to uninstall
139
- quiet: If True, suppress output
140
-
141
- Returns:
142
- True if successful, False otherwise
143
- """
144
- _ensure_logging_initialized()
145
- try:
146
- pip_args = [sys.executable, '-m', 'pip', 'uninstall', '-y', package_name]
147
-
148
- process = await asyncio.create_subprocess_exec(
149
- *pip_args,
150
- stdout=asyncio.subprocess.PIPE if quiet else None,
151
- stderr=asyncio.subprocess.PIPE if quiet else None
152
- )
153
-
154
- await process.communicate()
155
-
156
- if process.returncode == 0:
157
- if not quiet:
158
- logger.info(f"Uninstalled {package_name}")
159
- return True
160
- return False
161
- except Exception as e:
162
- logger.debug(f"Failed to uninstall {package_name}: {e}")
163
- return False
164
-
165
- __all__ = [
166
- 'get_package_size_mb',
167
- 'async_install_package',
168
- 'async_uninstall_package',
169
- ]
170
-
@@ -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
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
-