exonware-xwlazy 0.1.0.10__py3-none-any.whl → 0.1.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/version.py +2 -2
- exonware_xwlazy-0.1.0.11.dist-info/METADATA +380 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +20 -0
- xwlazy/__init__.py +34 -0
- xwlazy/lazy/__init__.py +301 -0
- xwlazy/lazy/bootstrap.py +106 -0
- xwlazy/lazy/config.py +163 -0
- xwlazy/lazy/host_conf.py +279 -0
- xwlazy/lazy/host_packages.py +122 -0
- xwlazy/lazy/lazy_base.py +465 -0
- xwlazy/lazy/lazy_contracts.py +290 -0
- xwlazy/lazy/lazy_core.py +3727 -0
- xwlazy/lazy/lazy_errors.py +271 -0
- xwlazy/lazy/lazy_state.py +86 -0
- xwlazy/lazy/logging_utils.py +194 -0
- xwlazy/lazy/manifest.py +489 -0
- xwlazy/version.py +77 -0
- exonware_xwlazy-0.1.0.10.dist-info/METADATA +0 -0
- exonware_xwlazy-0.1.0.10.dist-info/RECORD +0 -6
- {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.11.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.11.dist-info}/licenses/LICENSE +0 -0
xwlazy/lazy/manifest.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
xwlazy.lazy.manifest
|
|
5
|
+
--------------------
|
|
6
|
+
|
|
7
|
+
Centralized loader for per-package dependency manifests. A manifest can be
|
|
8
|
+
declared either as a JSON file located in the target project's root directory
|
|
9
|
+
or inline inside ``pyproject.toml`` under the ``[tool.xwlazy]`` namespace.
|
|
10
|
+
|
|
11
|
+
The loader consolidates the following pieces of information:
|
|
12
|
+
|
|
13
|
+
* Explicit import -> package mappings
|
|
14
|
+
* Serialization/watch prefixes that should be handled by the import hook
|
|
15
|
+
* Async installation preferences (queue enabled + worker count)
|
|
16
|
+
|
|
17
|
+
It also keeps lightweight caches with file-modification tracking so repeated
|
|
18
|
+
lookups do not hit the filesystem unnecessarily.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
import importlib.util
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from threading import RLock
|
|
27
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
|
28
|
+
|
|
29
|
+
try: # Python 3.11+
|
|
30
|
+
import tomllib # type: ignore[attr-defined]
|
|
31
|
+
except Exception: # pragma: no cover - fallback for <=3.10
|
|
32
|
+
try:
|
|
33
|
+
import tomli as tomllib # type: ignore[attr-defined]
|
|
34
|
+
except ImportError: # pragma: no cover
|
|
35
|
+
tomllib = None # type: ignore
|
|
36
|
+
|
|
37
|
+
DEFAULT_MANIFEST_FILENAMES: Tuple[str, ...] = (
|
|
38
|
+
"xwlazy.manifest.json",
|
|
39
|
+
"lazy.manifest.json",
|
|
40
|
+
".xwlazy.manifest.json",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
ENV_MANIFEST_PATH = "XWLAZY_MANIFEST_PATH"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _normalize_package_name(package_name: Optional[str]) -> str:
|
|
47
|
+
return (package_name or "global").strip().lower()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _normalize_prefix(prefix: str) -> str:
|
|
51
|
+
prefix = prefix.strip()
|
|
52
|
+
if not prefix:
|
|
53
|
+
return ""
|
|
54
|
+
if not prefix.endswith("."):
|
|
55
|
+
prefix = f"{prefix}."
|
|
56
|
+
return prefix
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_wrap_hints(values: Iterable[Any]) -> List[str]:
|
|
60
|
+
hints: List[str] = []
|
|
61
|
+
for value in values:
|
|
62
|
+
if value is None:
|
|
63
|
+
continue
|
|
64
|
+
hint = str(value).strip().lower()
|
|
65
|
+
if hint:
|
|
66
|
+
hints.append(hint)
|
|
67
|
+
return hints
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class PackageManifest:
|
|
72
|
+
"""Resolved manifest data for a single package."""
|
|
73
|
+
|
|
74
|
+
package: str
|
|
75
|
+
dependencies: Dict[str, str] = field(default_factory=dict)
|
|
76
|
+
watched_prefixes: Tuple[str, ...] = ()
|
|
77
|
+
async_installs: bool = False
|
|
78
|
+
async_workers: int = 1
|
|
79
|
+
class_wrap_prefixes: Tuple[str, ...] = ()
|
|
80
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
def get_dependency(self, import_name: str) -> Optional[str]:
|
|
83
|
+
"""Return the declared package for the given import name."""
|
|
84
|
+
if not import_name:
|
|
85
|
+
return None
|
|
86
|
+
direct = self.dependencies.get(import_name)
|
|
87
|
+
if direct is not None:
|
|
88
|
+
return direct
|
|
89
|
+
# Case-insensitive fallback for convenience
|
|
90
|
+
return self.dependencies.get(import_name.lower())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LazyManifestLoader:
|
|
94
|
+
"""
|
|
95
|
+
Loads and caches manifest data per package.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
default_root: Optional fallback root directory used when a package
|
|
99
|
+
root cannot be auto-detected.
|
|
100
|
+
package_roots: Optional explicit mapping used mainly for tests.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
default_root: Optional[Path] = None,
|
|
106
|
+
package_roots: Optional[Dict[str, Path]] = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
self._default_root = default_root
|
|
109
|
+
self._provided_roots = {
|
|
110
|
+
_normalize_package_name(name): Path(path)
|
|
111
|
+
for name, path in (package_roots or {}).items()
|
|
112
|
+
}
|
|
113
|
+
self._manifest_cache: Dict[str, PackageManifest] = {}
|
|
114
|
+
self._source_signatures: Dict[str, Tuple[str, float, float]] = {}
|
|
115
|
+
self._pyproject_cache: Dict[Path, Tuple[float, Dict[str, Any]]] = {}
|
|
116
|
+
self._shared_dependency_maps: Dict[
|
|
117
|
+
Tuple[str, float, float], Dict[str, Dict[str, str]]
|
|
118
|
+
] = {}
|
|
119
|
+
self._lock = RLock()
|
|
120
|
+
self._generation = 0
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def generation(self) -> int:
|
|
124
|
+
"""Incremented whenever any manifest content changes."""
|
|
125
|
+
return self._generation
|
|
126
|
+
|
|
127
|
+
def clear_cache(self) -> None:
|
|
128
|
+
"""Forcefully clear cached manifests."""
|
|
129
|
+
with self._lock:
|
|
130
|
+
self._manifest_cache.clear()
|
|
131
|
+
self._source_signatures.clear()
|
|
132
|
+
self._pyproject_cache.clear()
|
|
133
|
+
self._shared_dependency_maps.clear()
|
|
134
|
+
self._generation += 1
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------------------------- #
|
|
137
|
+
# Public API
|
|
138
|
+
# --------------------------------------------------------------------- #
|
|
139
|
+
def get_manifest(self, package_name: Optional[str]) -> Optional[PackageManifest]:
|
|
140
|
+
"""Return manifest for the provided package."""
|
|
141
|
+
key = _normalize_package_name(package_name)
|
|
142
|
+
with self._lock:
|
|
143
|
+
signature = self._compute_signature(key)
|
|
144
|
+
cached_signature = self._source_signatures.get(key)
|
|
145
|
+
if (
|
|
146
|
+
cached_signature is not None
|
|
147
|
+
and signature is not None
|
|
148
|
+
and cached_signature == signature
|
|
149
|
+
and key in self._manifest_cache
|
|
150
|
+
):
|
|
151
|
+
return self._manifest_cache[key]
|
|
152
|
+
|
|
153
|
+
manifest = self._load_manifest(key)
|
|
154
|
+
if manifest is None:
|
|
155
|
+
# Cache miss is still tracked so we don't re-read files
|
|
156
|
+
self._manifest_cache.pop(key, None)
|
|
157
|
+
self._source_signatures[key] = signature or ("", 0.0, 0.0)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
self._manifest_cache[key] = manifest
|
|
161
|
+
if signature is not None:
|
|
162
|
+
self._source_signatures[key] = signature
|
|
163
|
+
per_signature = self._shared_dependency_maps.setdefault(signature, {})
|
|
164
|
+
per_signature[manifest.package] = manifest.dependencies.copy()
|
|
165
|
+
self._generation += 1
|
|
166
|
+
return manifest
|
|
167
|
+
|
|
168
|
+
def get_manifest_signature(self, package_name: Optional[str]) -> Optional[Tuple[str, float, float]]:
|
|
169
|
+
key = _normalize_package_name(package_name)
|
|
170
|
+
with self._lock:
|
|
171
|
+
signature = self._source_signatures.get(key)
|
|
172
|
+
if signature is not None:
|
|
173
|
+
return signature
|
|
174
|
+
signature = self._compute_signature(key)
|
|
175
|
+
if signature is not None:
|
|
176
|
+
self._source_signatures[key] = signature
|
|
177
|
+
return signature
|
|
178
|
+
|
|
179
|
+
def get_shared_dependencies(
|
|
180
|
+
self,
|
|
181
|
+
package_name: Optional[str],
|
|
182
|
+
signature: Optional[Tuple[str, float, float]],
|
|
183
|
+
) -> Optional[Dict[str, str]]:
|
|
184
|
+
if signature is None:
|
|
185
|
+
return None
|
|
186
|
+
with self._lock:
|
|
187
|
+
package_maps = self._shared_dependency_maps.get(signature)
|
|
188
|
+
if not package_maps:
|
|
189
|
+
return None
|
|
190
|
+
key = _normalize_package_name(package_name)
|
|
191
|
+
return package_maps.get(key)
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------------ #
|
|
194
|
+
# Internal helpers
|
|
195
|
+
# ------------------------------------------------------------------ #
|
|
196
|
+
def _load_manifest(self, package_key: str) -> Optional[PackageManifest]:
|
|
197
|
+
root = self._resolve_project_root(package_key)
|
|
198
|
+
pyproject_path = root / "pyproject.toml"
|
|
199
|
+
pyproject_data = self._load_pyproject(pyproject_path)
|
|
200
|
+
json_data, manifest_path = self._load_json_manifest(root, pyproject_data)
|
|
201
|
+
|
|
202
|
+
data = self._merge_sources(package_key, pyproject_data, json_data)
|
|
203
|
+
if not data["dependencies"] and not data["watched_prefixes"] and not data["async_installs"]:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
wrap_prefixes = tuple(data.get("wrap_class_prefixes", ()))
|
|
207
|
+
|
|
208
|
+
manifest = PackageManifest(
|
|
209
|
+
package=package_key,
|
|
210
|
+
dependencies=data["dependencies"],
|
|
211
|
+
watched_prefixes=tuple(
|
|
212
|
+
_normalize_prefix(prefix)
|
|
213
|
+
for prefix in data["watched_prefixes"]
|
|
214
|
+
if _normalize_prefix(prefix)
|
|
215
|
+
),
|
|
216
|
+
async_installs=bool(data.get("async_installs")),
|
|
217
|
+
async_workers=max(1, int(data.get("async_workers", 1))),
|
|
218
|
+
class_wrap_prefixes=wrap_prefixes,
|
|
219
|
+
metadata={
|
|
220
|
+
"root": str(root),
|
|
221
|
+
"manifest_path": str(manifest_path) if manifest_path else None,
|
|
222
|
+
"wrap_class_prefixes": wrap_prefixes,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
return manifest
|
|
226
|
+
|
|
227
|
+
def _compute_signature(self, package_key: str) -> Optional[Tuple[str, float, float]]:
|
|
228
|
+
root = self._resolve_project_root(package_key)
|
|
229
|
+
pyproject_path = root / "pyproject.toml"
|
|
230
|
+
pyproject_mtime = pyproject_path.stat().st_mtime if pyproject_path.exists() else 0.0
|
|
231
|
+
manifest_path = self._resolve_manifest_path(root, pyproject_path)
|
|
232
|
+
json_mtime = manifest_path.stat().st_mtime if manifest_path and manifest_path.exists() else 0.0
|
|
233
|
+
env_token = os.environ.get(ENV_MANIFEST_PATH, "")
|
|
234
|
+
if not manifest_path and not pyproject_path.exists() and not env_token:
|
|
235
|
+
return None
|
|
236
|
+
return (env_token + str(manifest_path), pyproject_mtime, json_mtime)
|
|
237
|
+
|
|
238
|
+
def _resolve_project_root(self, package_key: str) -> Path:
|
|
239
|
+
if package_key in self._provided_roots:
|
|
240
|
+
return self._provided_roots[package_key]
|
|
241
|
+
|
|
242
|
+
module_candidates: Iterable[str]
|
|
243
|
+
if package_key == "global":
|
|
244
|
+
module_candidates = ()
|
|
245
|
+
else:
|
|
246
|
+
module_candidates = (f"exonware.{package_key}", package_key)
|
|
247
|
+
|
|
248
|
+
for module_name in module_candidates:
|
|
249
|
+
spec = importlib.util.find_spec(module_name)
|
|
250
|
+
if spec and spec.origin:
|
|
251
|
+
origin_path = Path(spec.origin).resolve()
|
|
252
|
+
root = self._walk_to_project_root(origin_path.parent)
|
|
253
|
+
if root:
|
|
254
|
+
self._provided_roots[package_key] = root
|
|
255
|
+
return root
|
|
256
|
+
|
|
257
|
+
if self._default_root:
|
|
258
|
+
return self._default_root
|
|
259
|
+
return Path.cwd()
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def _walk_to_project_root(start: Path) -> Optional[Path]:
|
|
263
|
+
current = start
|
|
264
|
+
while True:
|
|
265
|
+
if (current / "pyproject.toml").exists():
|
|
266
|
+
return current
|
|
267
|
+
parent = current.parent
|
|
268
|
+
if parent == current:
|
|
269
|
+
break
|
|
270
|
+
current = parent
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
# ------------------------------- #
|
|
274
|
+
# Pyproject helpers
|
|
275
|
+
# ------------------------------- #
|
|
276
|
+
def _load_pyproject(self, path: Path) -> Dict[str, Any]:
|
|
277
|
+
if not path.exists() or tomllib is None:
|
|
278
|
+
return {}
|
|
279
|
+
|
|
280
|
+
cached = self._pyproject_cache.get(path)
|
|
281
|
+
current_mtime = path.stat().st_mtime
|
|
282
|
+
if cached and cached[0] == current_mtime:
|
|
283
|
+
return cached[1]
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
with path.open("rb") as handle:
|
|
287
|
+
data = tomllib.load(handle)
|
|
288
|
+
except Exception:
|
|
289
|
+
data = {}
|
|
290
|
+
|
|
291
|
+
self._pyproject_cache[path] = (current_mtime, data)
|
|
292
|
+
return data
|
|
293
|
+
|
|
294
|
+
def _extract_pyproject_entry(
|
|
295
|
+
self,
|
|
296
|
+
pyproject_data: Dict[str, Any],
|
|
297
|
+
package_key: str,
|
|
298
|
+
) -> Dict[str, Any]:
|
|
299
|
+
tool_section = pyproject_data.get("tool", {})
|
|
300
|
+
lazy_section = tool_section.get("xwlazy", {})
|
|
301
|
+
packages = lazy_section.get("packages", {})
|
|
302
|
+
entry = packages.get(package_key, {}) or packages.get(package_key.upper(), {})
|
|
303
|
+
|
|
304
|
+
dependencies = {}
|
|
305
|
+
watched = []
|
|
306
|
+
async_installs = lazy_section.get("async_installs") or entry.get("async_installs")
|
|
307
|
+
async_workers = entry.get("async_workers") or lazy_section.get("async_workers")
|
|
308
|
+
wrap_hints = []
|
|
309
|
+
|
|
310
|
+
global_deps = lazy_section.get("dependencies", {})
|
|
311
|
+
if isinstance(global_deps, dict):
|
|
312
|
+
dependencies.update({str(k): str(v) for k, v in global_deps.items()})
|
|
313
|
+
|
|
314
|
+
if "dependencies" in entry and isinstance(entry["dependencies"], dict):
|
|
315
|
+
dependencies.update({str(k): str(v) for k, v in entry["dependencies"].items()})
|
|
316
|
+
|
|
317
|
+
for key in ("watched-prefixes", "watched_prefixes", "watch"):
|
|
318
|
+
values = entry.get(key) or lazy_section.get(key)
|
|
319
|
+
if isinstance(values, list):
|
|
320
|
+
watched.extend(str(v) for v in values)
|
|
321
|
+
|
|
322
|
+
global_wrap = lazy_section.get("wrap_class_prefixes") or lazy_section.get("wrap_classes")
|
|
323
|
+
if isinstance(global_wrap, list):
|
|
324
|
+
wrap_hints.extend(_normalize_wrap_hints(global_wrap))
|
|
325
|
+
entry_wrap = entry.get("wrap_class_prefixes") or entry.get("wrap_classes")
|
|
326
|
+
if isinstance(entry_wrap, list):
|
|
327
|
+
wrap_hints.extend(_normalize_wrap_hints(entry_wrap))
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"dependencies": dependencies,
|
|
331
|
+
"watched_prefixes": watched,
|
|
332
|
+
"async_installs": bool(async_installs),
|
|
333
|
+
"async_workers": async_workers or 1,
|
|
334
|
+
"wrap_class_prefixes": wrap_hints,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ------------------------------- #
|
|
338
|
+
# Manifest helpers
|
|
339
|
+
# ------------------------------- #
|
|
340
|
+
def _resolve_manifest_path(self, root: Path, pyproject_path: Path) -> Optional[Path]:
|
|
341
|
+
env_value = os.environ.get(ENV_MANIFEST_PATH)
|
|
342
|
+
if env_value:
|
|
343
|
+
for raw in env_value.split(os.pathsep):
|
|
344
|
+
candidate = Path(raw).expanduser()
|
|
345
|
+
if candidate.exists():
|
|
346
|
+
return candidate
|
|
347
|
+
|
|
348
|
+
if pyproject_path.exists() and tomllib is not None:
|
|
349
|
+
py_data = self._load_pyproject(pyproject_path)
|
|
350
|
+
tool_section = py_data.get("tool", {}).get("xwlazy", {})
|
|
351
|
+
manifest_path = tool_section.get("manifest") or tool_section.get("manifest_path")
|
|
352
|
+
if manifest_path:
|
|
353
|
+
candidate = (root / manifest_path).resolve()
|
|
354
|
+
if candidate.exists():
|
|
355
|
+
return candidate
|
|
356
|
+
|
|
357
|
+
for filename in DEFAULT_MANIFEST_FILENAMES:
|
|
358
|
+
candidate = root / filename
|
|
359
|
+
if candidate.exists():
|
|
360
|
+
return candidate
|
|
361
|
+
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
def _load_json_manifest(
|
|
365
|
+
self,
|
|
366
|
+
root: Path,
|
|
367
|
+
pyproject_data: Dict[str, Any],
|
|
368
|
+
) -> Tuple[Dict[str, Any], Optional[Path]]:
|
|
369
|
+
manifest_path = self._resolve_manifest_path(root, root / "pyproject.toml")
|
|
370
|
+
if not manifest_path:
|
|
371
|
+
return {}, None
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
with manifest_path.open("r", encoding="utf-8") as handle:
|
|
375
|
+
data = json.load(handle)
|
|
376
|
+
if isinstance(data, dict):
|
|
377
|
+
return data, manifest_path
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
return {}, manifest_path
|
|
382
|
+
|
|
383
|
+
def _merge_sources(
|
|
384
|
+
self,
|
|
385
|
+
package_key: str,
|
|
386
|
+
pyproject_data: Dict[str, Any],
|
|
387
|
+
json_data: Dict[str, Any],
|
|
388
|
+
) -> Dict[str, Any]:
|
|
389
|
+
merged_dependencies: Dict[str, str] = {}
|
|
390
|
+
merged_watched: List[str] = []
|
|
391
|
+
merged_wrap_hints: List[str] = []
|
|
392
|
+
|
|
393
|
+
# Pyproject first (acts as baseline)
|
|
394
|
+
py_entry = self._extract_pyproject_entry(pyproject_data, package_key)
|
|
395
|
+
merged_dependencies.update({k.lower(): v for k, v in py_entry["dependencies"].items()})
|
|
396
|
+
merged_watched.extend(py_entry["watched_prefixes"])
|
|
397
|
+
merged_wrap_hints.extend(_normalize_wrap_hints(py_entry.get("wrap_class_prefixes", [])))
|
|
398
|
+
|
|
399
|
+
async_installs = py_entry.get("async_installs", False)
|
|
400
|
+
async_workers = py_entry.get("async_workers", 1)
|
|
401
|
+
|
|
402
|
+
# JSON global settings
|
|
403
|
+
global_deps = json_data.get("dependencies", {})
|
|
404
|
+
if isinstance(global_deps, dict):
|
|
405
|
+
merged_dependencies.update({str(k).lower(): str(v) for k, v in global_deps.items()})
|
|
406
|
+
|
|
407
|
+
global_watch = json_data.get("watch") or json_data.get("watched_prefixes")
|
|
408
|
+
if isinstance(global_watch, list):
|
|
409
|
+
merged_watched.extend(str(item) for item in global_watch)
|
|
410
|
+
global_wrap = json_data.get("wrap_class_prefixes") or json_data.get("wrap_classes")
|
|
411
|
+
if isinstance(global_wrap, list):
|
|
412
|
+
merged_wrap_hints.extend(_normalize_wrap_hints(global_wrap))
|
|
413
|
+
|
|
414
|
+
global_async = json_data.get("async_installs")
|
|
415
|
+
if global_async is not None:
|
|
416
|
+
async_installs = bool(global_async)
|
|
417
|
+
global_workers = json_data.get("async_workers")
|
|
418
|
+
if global_workers is not None:
|
|
419
|
+
async_workers = global_workers
|
|
420
|
+
|
|
421
|
+
packages_section = json_data.get("packages", {})
|
|
422
|
+
if isinstance(packages_section, dict):
|
|
423
|
+
entry = packages_section.get(package_key) or packages_section.get(package_key.upper())
|
|
424
|
+
if isinstance(entry, dict):
|
|
425
|
+
entry_deps = entry.get("dependencies", {})
|
|
426
|
+
if isinstance(entry_deps, dict):
|
|
427
|
+
merged_dependencies.update({str(k).lower(): str(v) for k, v in entry_deps.items()})
|
|
428
|
+
|
|
429
|
+
entry_watch = entry.get("watched_prefixes") or entry.get("watch")
|
|
430
|
+
if isinstance(entry_watch, list):
|
|
431
|
+
merged_watched.extend(str(item) for item in entry_watch)
|
|
432
|
+
entry_wrap = entry.get("wrap_class_prefixes") or entry.get("wrap_classes")
|
|
433
|
+
if isinstance(entry_wrap, list):
|
|
434
|
+
merged_wrap_hints.extend(_normalize_wrap_hints(entry_wrap))
|
|
435
|
+
|
|
436
|
+
if "async_installs" in entry:
|
|
437
|
+
async_installs = bool(entry["async_installs"])
|
|
438
|
+
if "async_workers" in entry:
|
|
439
|
+
async_workers = entry.get("async_workers", async_workers)
|
|
440
|
+
|
|
441
|
+
seen_wrap: Set[str] = set()
|
|
442
|
+
ordered_wrap_hints: List[str] = []
|
|
443
|
+
for hint in merged_wrap_hints:
|
|
444
|
+
if hint not in seen_wrap:
|
|
445
|
+
seen_wrap.add(hint)
|
|
446
|
+
ordered_wrap_hints.append(hint)
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
"dependencies": merged_dependencies,
|
|
450
|
+
"watched_prefixes": merged_watched,
|
|
451
|
+
"async_installs": async_installs,
|
|
452
|
+
"async_workers": async_workers,
|
|
453
|
+
"wrap_class_prefixes": ordered_wrap_hints,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
_manifest_loader: Optional[LazyManifestLoader] = None
|
|
458
|
+
_manifest_loader_lock = RLock()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def get_manifest_loader() -> LazyManifestLoader:
|
|
462
|
+
"""
|
|
463
|
+
Return the process-wide manifest loader instance.
|
|
464
|
+
|
|
465
|
+
Calling this function does not force any manifest to be loaded, but the
|
|
466
|
+
loader keeps shared caches that allow multiple subsystems (dependency mapper,
|
|
467
|
+
installer, hook configuration) to observe manifest changes consistently.
|
|
468
|
+
"""
|
|
469
|
+
global _manifest_loader
|
|
470
|
+
with _manifest_loader_lock:
|
|
471
|
+
if _manifest_loader is None:
|
|
472
|
+
_manifest_loader = LazyManifestLoader()
|
|
473
|
+
return _manifest_loader
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def refresh_manifest_cache() -> None:
|
|
477
|
+
"""Forcefully clear the shared manifest loader cache."""
|
|
478
|
+
loader = get_manifest_loader()
|
|
479
|
+
loader.clear_cache()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
__all__ = [
|
|
483
|
+
"PackageManifest",
|
|
484
|
+
"LazyManifestLoader",
|
|
485
|
+
"get_manifest_loader",
|
|
486
|
+
"refresh_manifest_cache",
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
|
xwlazy/version.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized version management for eXonware projects.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
|
|
8
|
+
This module provides centralized version management for the entire project.
|
|
9
|
+
All version references should import from this module to ensure consistency.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# VERSION CONFIGURATION
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
# Main version - update this to change version across entire project
|
|
17
|
+
__version__ = "0.1.0.11"
|
|
18
|
+
|
|
19
|
+
# Version components for programmatic access
|
|
20
|
+
VERSION_MAJOR = 0
|
|
21
|
+
VERSION_MINOR = 1
|
|
22
|
+
VERSION_PATCH = 0
|
|
23
|
+
VERSION_BUILD = 11 # Set to None for releases, or build number for dev builds
|
|
24
|
+
|
|
25
|
+
# Version metadata
|
|
26
|
+
VERSION_SUFFIX = "" # e.g., "dev", "alpha", "beta", "rc1"
|
|
27
|
+
VERSION_STRING = __version__ + VERSION_SUFFIX
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# VERSION UTILITIES
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
def get_version() -> str:
|
|
34
|
+
"""Get the current version string."""
|
|
35
|
+
return VERSION_STRING
|
|
36
|
+
|
|
37
|
+
def get_version_info() -> tuple:
|
|
38
|
+
"""Get version as a tuple (major, minor, patch, build)."""
|
|
39
|
+
return (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_BUILD)
|
|
40
|
+
|
|
41
|
+
def get_version_dict() -> dict:
|
|
42
|
+
"""Get version information as a dictionary."""
|
|
43
|
+
return {
|
|
44
|
+
"version": VERSION_STRING,
|
|
45
|
+
"major": VERSION_MAJOR,
|
|
46
|
+
"minor": VERSION_MINOR,
|
|
47
|
+
"patch": VERSION_PATCH,
|
|
48
|
+
"build": VERSION_BUILD,
|
|
49
|
+
"suffix": VERSION_SUFFIX
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def is_dev_version() -> bool:
|
|
53
|
+
"""Check if this is a development version."""
|
|
54
|
+
return VERSION_SUFFIX in ("dev", "alpha", "beta") or VERSION_BUILD is not None
|
|
55
|
+
|
|
56
|
+
def is_release_version() -> bool:
|
|
57
|
+
"""Check if this is a release version."""
|
|
58
|
+
return not is_dev_version()
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# EXPORTS
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"__version__",
|
|
66
|
+
"VERSION_MAJOR",
|
|
67
|
+
"VERSION_MINOR",
|
|
68
|
+
"VERSION_PATCH",
|
|
69
|
+
"VERSION_BUILD",
|
|
70
|
+
"VERSION_SUFFIX",
|
|
71
|
+
"VERSION_STRING",
|
|
72
|
+
"get_version",
|
|
73
|
+
"get_version_info",
|
|
74
|
+
"get_version_dict",
|
|
75
|
+
"is_dev_version",
|
|
76
|
+
"is_release_version"
|
|
77
|
+
]
|
|
Binary file
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
exonware/xwlazy/__init__.py,sha256=ZXpGiNGjj5KRB6q4nDas863XgPcfOa89X8YTC2M_W2o,448
|
|
2
|
-
exonware/xwlazy/version.py,sha256=Y0qdE6fMxvnPdXCN7cBnCUDe9cVyriw9mVrZO6o6Ap0,2351
|
|
3
|
-
exonware_xwlazy-0.1.0.10.dist-info/METADATA,sha256=gPufZiVZr3HhSBQNpeZz1VS-HvQ-kUdm7AL7az399jI,1099
|
|
4
|
-
exonware_xwlazy-0.1.0.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
-
exonware_xwlazy-0.1.0.10.dist-info/licenses/LICENSE,sha256=w42ohoEUfhyT0NgiivAL4fWg2AMRLGnfXPMAR4EO-MU,1094
|
|
6
|
-
exonware_xwlazy-0.1.0.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|