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.
- nab_python/__init__.py +1 -0
- nab_python/_build/__init__.py +1 -0
- nab_python/_build/env.py +364 -0
- nab_python/_build/errors.py +17 -0
- nab_python/_build/runner.py +254 -0
- nab_python/_lockfile/__init__.py +1 -0
- nab_python/_lockfile/builder.py +339 -0
- nab_python/_lockfile/disjointness.py +207 -0
- nab_python/_lockfile/pylock.py +323 -0
- nab_python/_lockfile/requirements.py +121 -0
- nab_python/_packaging_provider.py +98 -0
- nab_python/_provider/__init__.py +1 -0
- nab_python/_provider/build_remote.py +95 -0
- nab_python/_provider/extras.py +231 -0
- nab_python/_provider/listing.py +442 -0
- nab_python/_provider/lookahead.py +156 -0
- nab_python/_provider/metadata_resolver.py +450 -0
- nab_python/_provider/priority.py +174 -0
- nab_python/_provider/sources.py +215 -0
- nab_python/_testing/__init__.py +1 -0
- nab_python/_testing/coordinator_fake.py +240 -0
- nab_python/_vcs_admission.py +209 -0
- nab_python/_vendor/__init__.py +6 -0
- nab_python/_vendor/packaging/LICENSE +3 -0
- nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
- nab_python/_vendor/packaging/LICENSE.BSD +23 -0
- nab_python/_vendor/packaging/PROVENANCE.md +73 -0
- nab_python/_vendor/packaging/__init__.py +15 -0
- nab_python/_vendor/packaging/_elffile.py +108 -0
- nab_python/_vendor/packaging/_manylinux.py +265 -0
- nab_python/_vendor/packaging/_musllinux.py +88 -0
- nab_python/_vendor/packaging/_parser.py +394 -0
- nab_python/_vendor/packaging/_structures.py +33 -0
- nab_python/_vendor/packaging/_tokenizer.py +196 -0
- nab_python/_vendor/packaging/dependency_groups.py +302 -0
- nab_python/_vendor/packaging/direct_url.py +325 -0
- nab_python/_vendor/packaging/errors.py +94 -0
- nab_python/_vendor/packaging/licenses/__init__.py +186 -0
- nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
- nab_python/_vendor/packaging/markers.py +506 -0
- nab_python/_vendor/packaging/metadata.py +964 -0
- nab_python/_vendor/packaging/py.typed +0 -0
- nab_python/_vendor/packaging/pylock.py +910 -0
- nab_python/_vendor/packaging/ranges.py +1803 -0
- nab_python/_vendor/packaging/requirements.py +132 -0
- nab_python/_vendor/packaging/specifiers.py +1141 -0
- nab_python/_vendor/packaging/tags.py +929 -0
- nab_python/_vendor/packaging/utils.py +296 -0
- nab_python/_vendor/packaging/version.py +1230 -0
- nab_python/build_backend.py +184 -0
- nab_python/config.py +805 -0
- nab_python/download.py +170 -0
- nab_python/fetch.py +827 -0
- nab_python/lockfile.py +238 -0
- nab_python/metadata.py +145 -0
- nab_python/provider.py +1235 -0
- nab_python/py.typed +0 -0
- nab_python/requirements_file.py +180 -0
- nab_python/resolve.py +497 -0
- nab_python/universal/__init__.py +1 -0
- nab_python/universal/matrix.py +235 -0
- nab_python/universal/provider.py +214 -0
- nab_python/universal/reresolve.py +310 -0
- nab_python/universal/resolve.py +508 -0
- nab_python/universal/validate.py +439 -0
- nab_python/universal/wheel_selection.py +327 -0
- nab_python/workspace.py +214 -0
- nab_python-0.0.1.dist-info/METADATA +49 -0
- nab_python-0.0.1.dist-info/RECORD +71 -0
- nab_python-0.0.1.dist-info/WHEEL +4 -0
- 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
|