nab-python 0.0.1__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 (71) hide show
  1. nab_python/__init__.py +1 -0
  2. nab_python/_build/__init__.py +1 -0
  3. nab_python/_build/env.py +364 -0
  4. nab_python/_build/errors.py +17 -0
  5. nab_python/_build/runner.py +254 -0
  6. nab_python/_lockfile/__init__.py +1 -0
  7. nab_python/_lockfile/builder.py +339 -0
  8. nab_python/_lockfile/disjointness.py +207 -0
  9. nab_python/_lockfile/pylock.py +323 -0
  10. nab_python/_lockfile/requirements.py +121 -0
  11. nab_python/_packaging_provider.py +98 -0
  12. nab_python/_provider/__init__.py +1 -0
  13. nab_python/_provider/build_remote.py +95 -0
  14. nab_python/_provider/extras.py +231 -0
  15. nab_python/_provider/listing.py +442 -0
  16. nab_python/_provider/lookahead.py +156 -0
  17. nab_python/_provider/metadata_resolver.py +450 -0
  18. nab_python/_provider/priority.py +174 -0
  19. nab_python/_provider/sources.py +215 -0
  20. nab_python/_testing/__init__.py +1 -0
  21. nab_python/_testing/coordinator_fake.py +240 -0
  22. nab_python/_vcs_admission.py +209 -0
  23. nab_python/_vendor/__init__.py +6 -0
  24. nab_python/_vendor/packaging/LICENSE +3 -0
  25. nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  26. nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  27. nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  28. nab_python/_vendor/packaging/__init__.py +15 -0
  29. nab_python/_vendor/packaging/_elffile.py +108 -0
  30. nab_python/_vendor/packaging/_manylinux.py +265 -0
  31. nab_python/_vendor/packaging/_musllinux.py +88 -0
  32. nab_python/_vendor/packaging/_parser.py +394 -0
  33. nab_python/_vendor/packaging/_structures.py +33 -0
  34. nab_python/_vendor/packaging/_tokenizer.py +196 -0
  35. nab_python/_vendor/packaging/dependency_groups.py +302 -0
  36. nab_python/_vendor/packaging/direct_url.py +325 -0
  37. nab_python/_vendor/packaging/errors.py +94 -0
  38. nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  39. nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  40. nab_python/_vendor/packaging/markers.py +506 -0
  41. nab_python/_vendor/packaging/metadata.py +964 -0
  42. nab_python/_vendor/packaging/py.typed +0 -0
  43. nab_python/_vendor/packaging/pylock.py +910 -0
  44. nab_python/_vendor/packaging/ranges.py +1803 -0
  45. nab_python/_vendor/packaging/requirements.py +132 -0
  46. nab_python/_vendor/packaging/specifiers.py +1141 -0
  47. nab_python/_vendor/packaging/tags.py +929 -0
  48. nab_python/_vendor/packaging/utils.py +296 -0
  49. nab_python/_vendor/packaging/version.py +1230 -0
  50. nab_python/build_backend.py +184 -0
  51. nab_python/config.py +805 -0
  52. nab_python/download.py +170 -0
  53. nab_python/fetch.py +827 -0
  54. nab_python/lockfile.py +238 -0
  55. nab_python/metadata.py +145 -0
  56. nab_python/provider.py +1235 -0
  57. nab_python/py.typed +0 -0
  58. nab_python/requirements_file.py +180 -0
  59. nab_python/resolve.py +497 -0
  60. nab_python/universal/__init__.py +1 -0
  61. nab_python/universal/matrix.py +235 -0
  62. nab_python/universal/provider.py +214 -0
  63. nab_python/universal/reresolve.py +310 -0
  64. nab_python/universal/resolve.py +508 -0
  65. nab_python/universal/validate.py +439 -0
  66. nab_python/universal/wheel_selection.py +327 -0
  67. nab_python/workspace.py +214 -0
  68. nab_python-0.0.1.dist-info/METADATA +49 -0
  69. nab_python-0.0.1.dist-info/RECORD +71 -0
  70. nab_python-0.0.1.dist-info/WHEEL +4 -0
  71. nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,439 @@
1
+ """Validate wheel metadata consistency across the resolved lock.
2
+
3
+ After ``resolve_universal`` produces a :class:`UniversalResult`, this
4
+ module fetches the metadata of the specific wheel each tuple would
5
+ install for each pinned package and checks the wheel's
6
+ ``Requires-Dist`` (after marker eval) against the deps the resolver
7
+ assumed. Two PyPI packages where wheels for one ``(name, version)``
8
+ disagree on deps drive the need: ``apache-beam`` (the win32 wheel
9
+ omits pyarrow that the linux wheels declare) and ``open3d``
10
+ (macos / linux / windows wheels diverge on addict, ipywidgets and
11
+ others). The matrix model bounds the cost to one extra metadata
12
+ fetch per ``(tuple, package)`` pair where the chosen wheel differs
13
+ from the resolver's baseline. See :class:`PinValidation` for the
14
+ per-pin status values and :meth:`ValidationReport.fatal_findings`
15
+ for the install-time fatality rules.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field
21
+ from typing import TYPE_CHECKING
22
+
23
+ from nab_index.client import WheelFile
24
+
25
+ from .._vendor.packaging.requirements import (
26
+ InvalidRequirement,
27
+ Requirement,
28
+ )
29
+ from .._vendor.packaging.utils import canonicalize_name
30
+ from ..metadata import DEPENDENCY_FIELDS, load_static_project, parse_metadata
31
+ from .wheel_selection import select_wheel_for_tuple
32
+
33
+ if TYPE_CHECKING:
34
+ from .._vendor.packaging.version import Version
35
+ from ..fetch import FetchCoordinator
36
+ from .matrix import MatrixTuple
37
+ from .resolve import UniversalResult
38
+
39
+
40
+ __all__ = [
41
+ "ExtraDiff",
42
+ "PinValidation",
43
+ "ValidationReport",
44
+ "validate_lock",
45
+ ]
46
+
47
+
48
+ # Statuses that always fail the lock at install time, regardless of
49
+ # build policy.
50
+ _ALWAYS_FATAL_STATUSES = frozenset({"no_compatible_wheel", "no_metadata"})
51
+
52
+ # Statuses that fail only when the build policy refuses to build
53
+ # from sdist (BuildPolicy.NEVER). These pins resolve fine if the
54
+ # user has a build toolchain.
55
+ _BUILD_REQUIRED_STATUSES = frozenset({"sdist_only", "no_compatible_wheel_with_sdist"})
56
+
57
+ # Metadata-Version 2.2 introduced PEP 643's Dynamic field. Earlier
58
+ # versions have no static-deps guarantee.
59
+ _MIN_STATIC_METADATA_VERSION = (2, 2)
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ExtraDiff:
64
+ """Per-extra divergence between baseline and chosen-wheel metadata.
65
+
66
+ ``extra_deps`` are deps the chosen wheel declares for ``extra``
67
+ that the baseline does not; ``missing_deps`` are the inverse. An
68
+ extra is included only when at least one of these is non-empty.
69
+ """
70
+
71
+ extra: str
72
+ extra_deps: tuple[str, ...] = ()
73
+ missing_deps: tuple[str, ...] = ()
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class PinValidation:
78
+ """Result of validating one ``(tuple, package, version)`` pin.
79
+
80
+ ``status`` is one of:
81
+
82
+ - ``ok``: chosen wheel's deps (after marker eval) match the resolver's.
83
+ - ``divergent``: wheel has metadata but deps differ from baseline.
84
+ - ``sdist_only``: no wheels at all; the user must build from sdist.
85
+ Fatal under ``BuildPolicy.NEVER``.
86
+ - ``no_compatible_wheel``: wheels exist but none match the tuple's
87
+ tags and no buildable sdist exists. Always fatal.
88
+ - ``no_compatible_wheel_with_sdist``: as above but a sdist is
89
+ available. Fatal under ``BuildPolicy.NEVER``.
90
+ - ``no_metadata``: the chosen wheel has no fetchable metadata.
91
+ Always fatal.
92
+ - ``static_sdist_authoritative``: the sdist's PEP 621 or PEP 643
93
+ metadata guarantees every wheel of this version shares the same
94
+ dep-affecting metadata, so per-wheel fetches were skipped.
95
+ """
96
+
97
+ tuple_label: str
98
+ package: str
99
+ version: str
100
+ status: str
101
+ chosen_wheel: str | None = None
102
+ detail: str = ""
103
+ extra_deps: tuple[str, ...] = ()
104
+ missing_deps: tuple[str, ...] = ()
105
+ extras_divergent: tuple[ExtraDiff, ...] = ()
106
+
107
+
108
+ @dataclass
109
+ class ValidationReport:
110
+ """Aggregate validation result for a UniversalResult."""
111
+
112
+ pins_checked: int = 0
113
+ pins_ok: int = 0
114
+ findings: list[PinValidation] = field(default_factory=list)
115
+
116
+ def fatal_findings(self, *, build_allowed: bool = False) -> list[PinValidation]:
117
+ """Return findings that would prevent installation.
118
+
119
+ Always fatal: ``no_compatible_wheel`` (no installable
120
+ artifact) and ``no_metadata`` (we can't trust the resolver
121
+ ran with the right deps).
122
+
123
+ Fatal under BuildPolicy.NEVER: ``sdist_only`` and
124
+ ``no_compatible_wheel_with_sdist`` (only sdist available).
125
+ """
126
+ return [
127
+ f
128
+ for f in self.findings
129
+ if f.status in _ALWAYS_FATAL_STATUSES
130
+ or (not build_allowed and f.status in _BUILD_REQUIRED_STATUSES)
131
+ ]
132
+
133
+
134
+ def validate_lock(
135
+ result: UniversalResult,
136
+ coordinator: FetchCoordinator,
137
+ ) -> ValidationReport:
138
+ """Validate every pin in ``result`` against its per-tuple wheel metadata.
139
+
140
+ ``coordinator`` is the same FetchCoordinator the resolver used.
141
+ Re-using it keeps cached metadata warm.
142
+ """
143
+ report = ValidationReport()
144
+ # ``pins_ok`` counts both per-wheel-validated successes and
145
+ # static-sdist-authoritative pins; both mean "the lock is sound
146
+ # for this pin", just established via different evidence.
147
+ ok_statuses = {"ok", "static_sdist_authoritative"}
148
+ for tr in result.tuple_results:
149
+ if not tr.success:
150
+ continue
151
+ for pkg, version in tr.pins.items():
152
+ finding = _validate_pin(coordinator, tr.tuple_, pkg, version)
153
+ report.findings.append(finding)
154
+ report.pins_checked += 1
155
+ if finding.status in ok_statuses:
156
+ report.pins_ok += 1
157
+ return report
158
+
159
+
160
+ def _validate_pin( # noqa: PLR0911 - one return per outcome reads cleaner here
161
+ coordinator: FetchCoordinator,
162
+ tup: MatrixTuple,
163
+ package: str,
164
+ version: Version,
165
+ ) -> PinValidation:
166
+ """Run the per-pin checks; emit a PinValidation outcome."""
167
+ normalized = canonicalize_name(package)
168
+ listing = coordinator.index.get_listing(normalized) or []
169
+ files_at_version = [f for f in listing if f.version == str(version)]
170
+ wheels_at_version = [f for f in files_at_version if isinstance(f, WheelFile)]
171
+ has_sdist = any(not isinstance(f, WheelFile) for f in files_at_version)
172
+ if not wheels_at_version:
173
+ return PinValidation(
174
+ tuple_label=tup.label,
175
+ package=package,
176
+ version=str(version),
177
+ status="sdist_only",
178
+ detail="no wheels at this version; install requires building from sdist",
179
+ )
180
+ chosen = select_wheel_for_tuple(
181
+ wheels_at_version,
182
+ python_version=tup.python_version,
183
+ spec=tup.platform_spec,
184
+ )
185
+ if chosen is None:
186
+ status = (
187
+ "no_compatible_wheel_with_sdist" if has_sdist else "no_compatible_wheel"
188
+ )
189
+ detail_suffix = (
190
+ "; sdist available so build-from-source is possible"
191
+ if has_sdist
192
+ else "; no sdist either, install will fail"
193
+ )
194
+ return PinValidation(
195
+ tuple_label=tup.label,
196
+ package=package,
197
+ version=str(version),
198
+ status=status,
199
+ detail=(
200
+ f"{len(wheels_at_version)} wheels at this version but none "
201
+ f"compatible with {tup.python_version}/{tup.platform_id}"
202
+ + detail_suffix
203
+ ),
204
+ )
205
+ # PEP 643 fast path: if the resolver's baseline metadata declares
206
+ # all dependency-affecting fields static (no Dynamic), every wheel
207
+ # built from the sdist MUST have the same Requires-Dist /
208
+ # Provides-Extra. Skip the per-wheel fetch.
209
+ if _baseline_has_static_deps(coordinator, normalized, version):
210
+ return PinValidation(
211
+ tuple_label=tup.label,
212
+ package=package,
213
+ version=str(version),
214
+ status="static_sdist_authoritative",
215
+ chosen_wheel=chosen.filename,
216
+ detail="PEP 643 static deps; all wheels guaranteed equal",
217
+ )
218
+ metadata_text = _fetch_wheel_metadata(coordinator, normalized, version, chosen)
219
+ if metadata_text is None:
220
+ return PinValidation(
221
+ tuple_label=tup.label,
222
+ package=package,
223
+ version=str(version),
224
+ status="no_metadata",
225
+ chosen_wheel=chosen.filename,
226
+ detail="wheel has no metadata file we could fetch",
227
+ )
228
+ chosen_by_extra = _evaluate_metadata_deps_by_extra(metadata_text, tup.environment)
229
+ listing_text = coordinator.index.get_metadata(normalized, str(version))
230
+ listing_by_extra: dict[str | None, set[str]] = (
231
+ _evaluate_metadata_deps_by_extra(listing_text, tup.environment)
232
+ if listing_text is not None
233
+ else {None: set()}
234
+ )
235
+ chosen_base = chosen_by_extra.get(None, set())
236
+ listing_base = listing_by_extra.get(None, set())
237
+ extra = sorted(chosen_base - listing_base)
238
+ missing = sorted(listing_base - chosen_base)
239
+ extras_divergent = _per_extra_divergence(chosen_by_extra, listing_by_extra)
240
+
241
+ if not extra and not missing and not extras_divergent:
242
+ return PinValidation(
243
+ tuple_label=tup.label,
244
+ package=package,
245
+ version=str(version),
246
+ status="ok",
247
+ chosen_wheel=chosen.filename,
248
+ )
249
+ if extra or missing:
250
+ return PinValidation(
251
+ tuple_label=tup.label,
252
+ package=package,
253
+ version=str(version),
254
+ status="divergent",
255
+ chosen_wheel=chosen.filename,
256
+ detail=(
257
+ f"chosen wheel differs from listing baseline: "
258
+ f"+{len(extra)} extra, -{len(missing)} missing deps"
259
+ ),
260
+ extra_deps=tuple(extra),
261
+ missing_deps=tuple(missing),
262
+ extras_divergent=extras_divergent,
263
+ )
264
+ return PinValidation(
265
+ tuple_label=tup.label,
266
+ package=package,
267
+ version=str(version),
268
+ status="divergent_in_extra",
269
+ chosen_wheel=chosen.filename,
270
+ detail=(
271
+ f"chosen wheel diverges in {len(extras_divergent)} "
272
+ f"extra(s): {', '.join(d.extra for d in extras_divergent)}"
273
+ ),
274
+ extras_divergent=extras_divergent,
275
+ )
276
+
277
+
278
+ def _per_extra_divergence(
279
+ chosen: dict[str | None, set[str]],
280
+ baseline: dict[str | None, set[str]],
281
+ ) -> tuple[ExtraDiff, ...]:
282
+ """Compare per-extra deps between chosen wheel and baseline.
283
+
284
+ Each extra (in either dict, excluding ``None``) is a candidate.
285
+ An extra is reported only when its chosen-vs-baseline diff is
286
+ non-empty. An extra present in only one side is still compared:
287
+ the absent side contributes the empty set.
288
+ """
289
+ extras = {e for e in chosen.keys() | baseline.keys() if e is not None}
290
+ diffs: list[ExtraDiff] = []
291
+ for extra in sorted(extras):
292
+ c = chosen.get(extra, set())
293
+ b = baseline.get(extra, set())
294
+ extra_deps = tuple(sorted(c - b))
295
+ missing_deps = tuple(sorted(b - c))
296
+ if extra_deps or missing_deps:
297
+ diffs.append(
298
+ ExtraDiff(extra=extra, extra_deps=extra_deps, missing_deps=missing_deps)
299
+ )
300
+ return tuple(diffs)
301
+
302
+
303
+ def _baseline_has_static_deps(
304
+ coordinator: FetchCoordinator,
305
+ normalized: str,
306
+ version: Version,
307
+ ) -> bool:
308
+ """Return True if baseline declares deps fully static.
309
+
310
+ Two qualifying routes:
311
+
312
+ 1. PEP 643: the METADATA is Version 2.2+ and ``Dynamic`` does not
313
+ include ``Requires-Dist`` or ``Provides-Extra``. Every wheel
314
+ built from this sdist must share those fields.
315
+ 2. PEP 621 pyproject.toml: the sdist contains a ``pyproject.toml``
316
+ with a ``[project]`` table that defines ``dependencies`` (and
317
+ ``optional-dependencies`` if used) statically (not listed in
318
+ ``[project].dynamic``). Per PEP 621, build backends must honour
319
+ the declared values. This route covers older Metadata (pre-2.2)
320
+ and backends that mark fields Dynamic in METADATA even when
321
+ pyproject.toml is static.
322
+
323
+ Returns False when neither route qualifies.
324
+ """
325
+ if _metadata_is_pep643_static(coordinator, normalized, version):
326
+ return True
327
+ return _pyproject_is_pep621_static(coordinator, normalized, version)
328
+
329
+
330
+ def _metadata_is_pep643_static(
331
+ coordinator: FetchCoordinator,
332
+ normalized: str,
333
+ version: Version,
334
+ ) -> bool:
335
+ """Route 1: PEP 643 Metadata 2.2+ without dependency Dynamic fields."""
336
+ text = coordinator.index.get_metadata(normalized, str(version))
337
+ if text is None:
338
+ return False
339
+ try:
340
+ metadata = parse_metadata(text)
341
+ except Exception: # noqa: BLE001
342
+ return False
343
+ if metadata.metadata_version is None:
344
+ return False
345
+ try:
346
+ major, minor = (int(p) for p in metadata.metadata_version.split(".", 1))
347
+ except ValueError:
348
+ return False
349
+ if (major, minor) < _MIN_STATIC_METADATA_VERSION:
350
+ return False
351
+ return not (DEPENDENCY_FIELDS & metadata.dynamic)
352
+
353
+
354
+ def _pyproject_is_pep621_static(
355
+ coordinator: FetchCoordinator,
356
+ normalized: str,
357
+ version: Version,
358
+ ) -> bool:
359
+ """Route 2: sdist pyproject.toml has static ``[project].dependencies``.
360
+
361
+ The coordinator caches pyproject.toml text via
362
+ :meth:`InMemoryIndex.get_sdist_pyproject` whenever it fetches
363
+ an sdist. For wheels-only resolves, no sdist fetch happens, so
364
+ this returns False; callers fall through to the per-wheel
365
+ validation path.
366
+
367
+ PEP 621 says ``[project].dependencies`` is static UNLESS
368
+ ``dependencies`` appears in ``[project].dynamic``. When neither
369
+ ``dependencies`` nor ``optional-dependencies`` sits in the
370
+ dynamic set, the values in pyproject.toml are authoritative for
371
+ every wheel built from this sdist. The keys themselves may be
372
+ absent: PEP 621 treats that as "no deps", which is itself
373
+ static.
374
+ """
375
+ text = coordinator.index.get_sdist_pyproject(normalized, str(version))
376
+ if text is None:
377
+ return False
378
+ return load_static_project(text) is not None
379
+
380
+
381
+ def _fetch_wheel_metadata(
382
+ coordinator: FetchCoordinator,
383
+ normalized: str,
384
+ version: Version,
385
+ wheel: WheelFile,
386
+ ) -> str | None:
387
+ """Fetch wheel-specific metadata via the coordinator's transport.
388
+
389
+ Uses :meth:`FetchCoordinator.request_wheel_metadata` which submits
390
+ through the same async fetcher the resolver uses, sharing
391
+ connection pooling and the on-disk cache. Cache key is
392
+ ``(name, "<version>#<filename>")`` so the resolver-time
393
+ ``(name, version)`` cache is undisturbed.
394
+
395
+ Returns ``None`` if the wheel has no PEP 658 ``metadata_url`` or
396
+ the fetch failed.
397
+ """
398
+ if wheel.metadata_url is None:
399
+ return None
400
+ event = coordinator.request_wheel_metadata(
401
+ normalized, str(version), wheel.filename, wheel.metadata_url
402
+ )
403
+ event.wait()
404
+ return coordinator.index.get_metadata(normalized, f"{version}#{wheel.filename}")
405
+
406
+
407
+ def _evaluate_metadata_deps_by_extra(
408
+ metadata_text: str,
409
+ environment: dict[str, str],
410
+ ) -> dict[str | None, set[str]]:
411
+ """Return deps grouped by extra: ``{None: base_deps, "extra": deps_for_extra}``.
412
+
413
+ Each ``Requires-Dist`` is bucketed by which ``extra`` setting (if
414
+ any) makes its marker evaluate True. Markers without ``extra``
415
+ references go to the base bucket; markers with ``extra ==
416
+ "name"`` go to that named bucket. This lets the validator catch
417
+ per-extra divergence between the resolver's listing baseline and
418
+ the chosen wheel, even when base deps match.
419
+ """
420
+ metadata = parse_metadata(metadata_text)
421
+ extras = {canonicalize_name(e) for e in metadata.provides_extra}
422
+ out: dict[str | None, set[str]] = {None: set()}
423
+ for e in extras:
424
+ out[e] = set()
425
+ base_env = {**environment, "extra": ""}
426
+ for req_text in metadata.requires_dist:
427
+ try:
428
+ req = Requirement(str(req_text))
429
+ except InvalidRequirement: # pragma: no cover
430
+ continue
431
+ name = canonicalize_name(req.name)
432
+ marker = req.marker
433
+ if marker is None or marker.evaluate(base_env):
434
+ out[None].add(name)
435
+ continue
436
+ for e in extras:
437
+ if marker.evaluate({**environment, "extra": e}):
438
+ out[e].add(name)
439
+ return out