nab-python 0.0.5__py3-none-any.whl → 0.0.6__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/_build/env.py +17 -9
- nab_python/_build/runner.py +2 -2
- nab_python/_conflict_kind.py +42 -0
- nab_python/_lockfile/builder.py +81 -38
- nab_python/_lockfile/disjointness.py +5 -5
- nab_python/_lockfile/pylock.py +17 -4
- nab_python/_lockfile/requirements.py +5 -1
- nab_python/_provider/build_remote.py +39 -4
- nab_python/_provider/extras.py +6 -0
- nab_python/_provider/listing.py +170 -48
- nab_python/_provider/metadata_resolver.py +121 -19
- nab_python/_provider/sources.py +3 -5
- nab_python/_testing/coordinator_fake.py +6 -1
- nab_python/_testing/overrides.py +18 -0
- nab_python/_vcs_admission.py +30 -11
- nab_python/_vendor/packaging/PROVENANCE.md +48 -29
- nab_python/_vendor/packaging/_elffile.py +5 -5
- nab_python/_vendor/packaging/_manylinux.py +30 -15
- nab_python/_vendor/packaging/_parser.py +21 -4
- nab_python/_vendor/packaging/_ranges.py +836 -0
- nab_python/_vendor/packaging/_tokenizer.py +5 -1
- nab_python/_vendor/packaging/dependency_groups.py +29 -1
- nab_python/_vendor/packaging/direct_url.py +19 -4
- nab_python/_vendor/packaging/licenses/__init__.py +18 -6
- nab_python/_vendor/packaging/markers.py +29 -6
- nab_python/_vendor/packaging/metadata.py +3 -2
- nab_python/_vendor/packaging/pylock.py +6 -1
- nab_python/_vendor/packaging/ranges.py +707 -1513
- nab_python/_vendor/packaging/requirements.py +60 -20
- nab_python/_vendor/packaging/specifiers.py +290 -374
- nab_python/_vendor/packaging/tags.py +106 -43
- nab_python/_vendor/packaging/utils.py +33 -5
- nab_python/build_backend.py +11 -3
- nab_python/config.py +1142 -385
- nab_python/config_sources.py +1799 -0
- nab_python/download.py +41 -12
- nab_python/fetch.py +152 -72
- nab_python/lockfile.py +59 -5
- nab_python/metadata.py +5 -4
- nab_python/provider.py +459 -82
- nab_python/requirements_file.py +143 -18
- nab_python/resolve.py +166 -54
- nab_python/universal/matrix.py +0 -2
- nab_python/universal/provider.py +7 -8
- nab_python/universal/reresolve.py +27 -21
- nab_python/universal/resolve.py +62 -50
- nab_python/universal/validate.py +95 -22
- nab_python/universal/wheel_selection.py +67 -19
- nab_python/workspace.py +8 -1
- {nab_python-0.0.5.dist-info → nab_python-0.0.6.dist-info}/METADATA +3 -3
- nab_python-0.0.6.dist-info/RECORD +75 -0
- nab_python/_vendor/packaging/_range_utils.py +0 -998
- nab_python/_vendor/packaging/_version_utils.py +0 -90
- nab_python-0.0.5.dist-info/RECORD +0 -74
- {nab_python-0.0.5.dist-info → nab_python-0.0.6.dist-info}/WHEEL +0 -0
nab_python/_build/env.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ``venv.EnvBuilder`` (stdlib) creates an empty interpreter at a temp
|
|
7
7
|
path, ``with_pip=False``; nab does not need pip in there.
|
|
8
8
|
* nab's own resolver picks versions for ``[build-system].requires``
|
|
9
|
-
using the same indexes / ``
|
|
9
|
+
using the same indexes / ``uploaded-prior-to`` window as the outer
|
|
10
10
|
resolve.
|
|
11
11
|
* ``download_lock`` from :mod:`nab_python.download` fetches the
|
|
12
12
|
resolved wheels into a temp directory.
|
|
@@ -37,6 +37,7 @@ from installer.destinations import SchemeDictionaryDestination
|
|
|
37
37
|
from installer.sources import WheelFile
|
|
38
38
|
from nab_index.urllib3_async_transport import Urllib3AsyncTransport
|
|
39
39
|
|
|
40
|
+
from .._vcs_admission import UnsupportedVcsError
|
|
40
41
|
from ..config import NabProjectConfig
|
|
41
42
|
from ..download import download_lock
|
|
42
43
|
|
|
@@ -97,7 +98,7 @@ class NabBuildEnv:
|
|
|
97
98
|
|
|
98
99
|
``requires`` is the PEP 508 string list from
|
|
99
100
|
``[build-system].requires``. ``config`` carries the indexes,
|
|
100
|
-
``
|
|
101
|
+
``uploaded-prior-to`` window and other nab inputs from the outer
|
|
101
102
|
resolve; it is pruned (no local sources, no workspace, no
|
|
102
103
|
marker overlay) before the inner resolve so the build env is
|
|
103
104
|
computed against PyPI alone.
|
|
@@ -269,20 +270,27 @@ class NabBuildEnv:
|
|
|
269
270
|
|
|
270
271
|
inner_config = NabProjectConfig(
|
|
271
272
|
indexes=self._config.indexes,
|
|
273
|
+
package_overrides=self._config.package_overrides,
|
|
272
274
|
index_overrides=self._config.index_overrides,
|
|
273
275
|
uploaded_prior_to=self._config.uploaded_prior_to,
|
|
274
|
-
uploaded_prior_to_overrides=self._config.uploaded_prior_to_overrides,
|
|
275
276
|
)
|
|
276
277
|
# download_lock closes its transport, and ``install`` may call
|
|
277
278
|
# this again for ``get_requires_for_build_wheel`` follow-ups;
|
|
278
279
|
# build a fresh transport each time.
|
|
279
280
|
transport = self._transport_factory()
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
try:
|
|
282
|
+
result = resolve_pyproject(
|
|
283
|
+
synthetic,
|
|
284
|
+
transport,
|
|
285
|
+
config=inner_config,
|
|
286
|
+
python_version=self._python_version,
|
|
287
|
+
)
|
|
288
|
+
except (UnsupportedVcsError, NotImplementedError) as exc:
|
|
289
|
+
# A direct-URL/VCS build requirement means nab cannot build this
|
|
290
|
+
# sdist. Report it as a build-env failure so the outer resolve
|
|
291
|
+
# skips the version instead of aborting on the raw error.
|
|
292
|
+
msg = f"build env resolve failed: {exc}"
|
|
293
|
+
raise BuildEnvError(msg) from exc
|
|
286
294
|
|
|
287
295
|
# Reject sdist-only pins early: build deps that ship only an
|
|
288
296
|
# sdist trigger a recursive backend invocation that this
|
nab_python/_build/runner.py
CHANGED
|
@@ -250,9 +250,9 @@ def _parse_metadata(metadata_path: Path) -> WheelMetadata:
|
|
|
250
250
|
|
|
251
251
|
provides_extra: list[str] = sorted(
|
|
252
252
|
{
|
|
253
|
-
canonicalize_name(
|
|
253
|
+
canonicalize_name(stripped)
|
|
254
254
|
for extra in msg_obj.get_all("Provides-Extra") or ()
|
|
255
|
-
if extra
|
|
255
|
+
if (stripped := extra.strip())
|
|
256
256
|
}
|
|
257
257
|
)
|
|
258
258
|
|
nab_python/_conflict_kind.py
CHANGED
|
@@ -8,6 +8,14 @@ cycle. :class:`nab_python.config.ConflictKind` takes its enum values from
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import re
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
|
|
17
|
+
from ._vendor.packaging.markers import Marker
|
|
18
|
+
|
|
11
19
|
KIND_EXTRA = "extra"
|
|
12
20
|
KIND_GROUP = "group"
|
|
13
21
|
|
|
@@ -18,3 +26,37 @@ MARKER_VARIABLE_FOR_KIND = {
|
|
|
18
26
|
KIND_EXTRA: "extras",
|
|
19
27
|
KIND_GROUP: "dependency_groups",
|
|
20
28
|
}
|
|
29
|
+
|
|
30
|
+
# ``extras`` and ``dependency_groups`` are PEP 685 / PEP 735 set variables that
|
|
31
|
+
# packaging only defines when consuming a lockfile. At resolve time no
|
|
32
|
+
# conflict-fork member is active as a marker-set member (forks fold their
|
|
33
|
+
# members into requirements, not the environment), so both are empty. Seeding
|
|
34
|
+
# them keeps a dependency marker that tests one from raising a raw KeyError at
|
|
35
|
+
# evaluation; the membership simply tests False and the dep is dropped.
|
|
36
|
+
EMPTY_MEMBERSHIP_SETS: dict[str, frozenset[str]] = {
|
|
37
|
+
variable: frozenset() for variable in MARKER_VARIABLE_FOR_KIND.values()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_MEMBERSHIP_SET_PATTERN = re.compile(
|
|
41
|
+
r"\b(" + "|".join(MARKER_VARIABLE_FOR_KIND.values()) + r")\b"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def membership_set_in_marker(marker_text: str) -> str | None:
|
|
46
|
+
"""Return the lockfile-only set variable a marker tests, or ``None``.
|
|
47
|
+
|
|
48
|
+
A dependency marker that tests ``extras`` or ``dependency_groups`` is a
|
|
49
|
+
mistake (usually meant as ``extra ==``): those variables are defined only
|
|
50
|
+
when consuming a lockfile, so they are empty at resolve time.
|
|
51
|
+
"""
|
|
52
|
+
match = _MEMBERSHIP_SET_PATTERN.search(marker_text)
|
|
53
|
+
return match.group(1) if match else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def dependency_marker_holds(marker: Marker, environment: Mapping[str, str]) -> bool:
|
|
57
|
+
"""Evaluate a dependency marker for a resolve-time ``environment``.
|
|
58
|
+
|
|
59
|
+
Seeds the empty lockfile-only set variables so a marker that tests one
|
|
60
|
+
evaluates cleanly to False instead of raising a raw KeyError.
|
|
61
|
+
"""
|
|
62
|
+
return bool(marker.evaluate({**environment, **EMPTY_MEMBERSHIP_SETS}))
|
nab_python/_lockfile/builder.py
CHANGED
|
@@ -13,7 +13,7 @@ from collections import defaultdict
|
|
|
13
13
|
from datetime import datetime, timezone
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Protocol, overload
|
|
16
|
-
from urllib.parse import urlsplit, urlunsplit
|
|
16
|
+
from urllib.parse import quote, urlsplit, urlunsplit
|
|
17
17
|
|
|
18
18
|
import tomli
|
|
19
19
|
|
|
@@ -47,6 +47,7 @@ __all__ = [
|
|
|
47
47
|
"build_lock_input_from_provider",
|
|
48
48
|
"read_lockfile_anchor",
|
|
49
49
|
"read_lockfile_packages",
|
|
50
|
+
"require_artifact_hashes",
|
|
50
51
|
]
|
|
51
52
|
|
|
52
53
|
|
|
@@ -104,8 +105,16 @@ class LockInputProvider(Protocol):
|
|
|
104
105
|
"""Return the listing slice that matches ``(canonical_name, version)``."""
|
|
105
106
|
...
|
|
106
107
|
|
|
107
|
-
def effective_dist_policy(
|
|
108
|
-
|
|
108
|
+
def effective_dist_policy(
|
|
109
|
+
self, canonical_name: str, version: Version, index_name: str | None = None, /
|
|
110
|
+
) -> DistPolicy:
|
|
111
|
+
"""Return the effective :class:`DistPolicy` for ``canonical_name==version``."""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def effective_requires_python(
|
|
115
|
+
self, canonical_name: str, version: Version, /
|
|
116
|
+
) -> str | None:
|
|
117
|
+
"""Return the ``requires-python`` override for ``canonical_name==version``."""
|
|
109
118
|
...
|
|
110
119
|
|
|
111
120
|
|
|
@@ -205,17 +214,23 @@ def read_lockfile_packages(path: Path) -> dict[str, Version] | None:
|
|
|
205
214
|
|
|
206
215
|
|
|
207
216
|
def _strip_userinfo(url: str) -> str:
|
|
208
|
-
"""Return ``url`` with
|
|
209
|
-
|
|
210
|
-
Lockfiles are committed to version control, so an index or VCS
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
and
|
|
214
|
-
|
|
217
|
+
"""Return ``url`` with credential userinfo removed.
|
|
218
|
+
|
|
219
|
+
Lockfiles are committed to version control, so an index or VCS URL
|
|
220
|
+
carrying an embedded ``user:password`` must not be written verbatim.
|
|
221
|
+
An SSH login such as ``git@`` is the protocol login, not a secret,
|
|
222
|
+
and is required to clone, so it is kept (only an embedded password is
|
|
223
|
+
dropped); host case and port are preserved. A no-op for URLs without
|
|
224
|
+
userinfo.
|
|
215
225
|
"""
|
|
216
226
|
parts = urlsplit(url)
|
|
217
|
-
|
|
218
|
-
|
|
227
|
+
userinfo, sep, host = parts.netloc.rpartition("@")
|
|
228
|
+
if not sep:
|
|
229
|
+
return url
|
|
230
|
+
if parts.scheme.endswith("ssh"):
|
|
231
|
+
login = userinfo.split(":", 1)[0]
|
|
232
|
+
host = f"{login}@{host}"
|
|
233
|
+
return urlunsplit(parts._replace(netloc=host))
|
|
219
234
|
|
|
220
235
|
|
|
221
236
|
def build_lock_input_from_provider( # noqa: PLR0913 - each flag maps to a distinct lockfile field
|
|
@@ -348,13 +363,23 @@ def _index_pin_from_listing(
|
|
|
348
363
|
package's wheels stayed in ``versions_cache`` as a possible
|
|
349
364
|
metadata source for the resolver; only the sdist is emitted
|
|
350
365
|
into the lock so installers download and build that archive.
|
|
366
|
+
|
|
367
|
+
A ``requires-python`` metadata override takes precedence over the
|
|
368
|
+
Simple-API value so the pin records the specifier the resolver
|
|
369
|
+
actually admitted against; a conforming :pep:`751` installer would
|
|
370
|
+
otherwise reject a widened pin whose lock still carried the narrow
|
|
371
|
+
artefact value.
|
|
351
372
|
"""
|
|
352
373
|
from ..fetch import DEFAULT_INDEX_URL
|
|
353
374
|
from ..lockfile import IndexPin, SdistArtifact, WheelArtifact
|
|
354
375
|
from ..provider import DistPolicy
|
|
355
376
|
|
|
356
377
|
files = list(provider.dist_files_for(canonical, version))
|
|
357
|
-
|
|
378
|
+
serving = provider.coordinator.index.get_listing_index(canonical)
|
|
379
|
+
if (
|
|
380
|
+
provider.effective_dist_policy(canonical, version, serving)
|
|
381
|
+
is DistPolicy.SDIST_INSTALL
|
|
382
|
+
):
|
|
358
383
|
files = [f for f in files if not isinstance(f, WheelFile)]
|
|
359
384
|
if not any(isinstance(f, SdistFile) for f in files):
|
|
360
385
|
msg = (
|
|
@@ -365,17 +390,18 @@ def _index_pin_from_listing(
|
|
|
365
390
|
raise MissingSdistError(msg)
|
|
366
391
|
|
|
367
392
|
wheels = tuple(
|
|
368
|
-
_build_artifact(
|
|
369
|
-
for f in files
|
|
370
|
-
if isinstance(f, WheelFile)
|
|
393
|
+
_build_artifact(f, WheelArtifact) for f in files if isinstance(f, WheelFile)
|
|
371
394
|
)
|
|
372
395
|
sdist_file = next((f for f in files if isinstance(f, SdistFile)), None)
|
|
373
396
|
sdist = (
|
|
374
|
-
_build_artifact(
|
|
375
|
-
if sdist_file is not None
|
|
376
|
-
else None
|
|
397
|
+
_build_artifact(sdist_file, SdistArtifact) if sdist_file is not None else None
|
|
377
398
|
)
|
|
378
|
-
|
|
399
|
+
|
|
400
|
+
override_rp = provider.effective_requires_python(canonical, version)
|
|
401
|
+
requires_python = (
|
|
402
|
+
override_rp if override_rp is not None else _common_requires_python(files)
|
|
403
|
+
)
|
|
404
|
+
|
|
379
405
|
serving_name = provider.coordinator.index.get_listing_index(canonical)
|
|
380
406
|
by_name = {ix.name: ix.url for ix in indexes}
|
|
381
407
|
index_url = by_name.get(serving_name) if serving_name is not None else None
|
|
@@ -402,25 +428,22 @@ def _index_pin_from_listing(
|
|
|
402
428
|
|
|
403
429
|
@overload
|
|
404
430
|
def _build_artifact(
|
|
405
|
-
canonical: str,
|
|
406
431
|
source: WheelFile | SdistFile,
|
|
407
432
|
cls: type[WheelArtifact],
|
|
408
433
|
) -> WheelArtifact: ...
|
|
409
434
|
@overload
|
|
410
435
|
def _build_artifact(
|
|
411
|
-
canonical: str,
|
|
412
436
|
source: WheelFile | SdistFile,
|
|
413
437
|
cls: type[SdistArtifact],
|
|
414
438
|
) -> SdistArtifact: ...
|
|
415
439
|
def _build_artifact(
|
|
416
|
-
canonical: str,
|
|
417
440
|
source: WheelFile | SdistFile,
|
|
418
441
|
cls: type[WheelArtifact | SdistArtifact],
|
|
419
442
|
) -> WheelArtifact | SdistArtifact:
|
|
420
|
-
hashes = _filter_acceptable_hashes(
|
|
443
|
+
hashes = _filter_acceptable_hashes(source.hashes)
|
|
421
444
|
return cls(
|
|
422
445
|
filename=source.filename,
|
|
423
|
-
url=source.url,
|
|
446
|
+
url=_strip_userinfo(source.url),
|
|
424
447
|
hashes=hashes,
|
|
425
448
|
size=source.size,
|
|
426
449
|
upload_time=_parse_upload_time(source.upload_time),
|
|
@@ -449,30 +472,50 @@ def _parse_upload_time(raw: str | None) -> datetime | None:
|
|
|
449
472
|
|
|
450
473
|
|
|
451
474
|
def _filter_acceptable_hashes(
|
|
452
|
-
|
|
475
|
+
hashes: tuple[tuple[str, str], ...],
|
|
453
476
|
) -> tuple[tuple[str, str], ...]:
|
|
454
477
|
"""Return the subset of ``hashes`` whose algorithm is consumable.
|
|
455
478
|
|
|
456
479
|
Pip's hash-checking mode and PEP 751 both accept any of sha256,
|
|
457
480
|
sha384, or sha512; nab forwards every recorded entry so consumers
|
|
458
|
-
can pick.
|
|
459
|
-
|
|
481
|
+
can pick. Unacceptable algorithms (e.g. md5) are dropped, so the
|
|
482
|
+
result may be empty.
|
|
460
483
|
"""
|
|
461
484
|
from ..lockfile import ACCEPTED_HASH_ALGORITHMS
|
|
462
485
|
|
|
463
|
-
|
|
486
|
+
return tuple(
|
|
464
487
|
(algo, digest)
|
|
465
488
|
for algo, digest in sorted(hashes)
|
|
466
489
|
if algo in ACCEPTED_HASH_ALGORITHMS
|
|
467
490
|
)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def require_artifact_hashes(lock_input: LockInput) -> None:
|
|
494
|
+
"""Raise :class:`MissingHashError` if a pinned artefact has no hash.
|
|
495
|
+
|
|
496
|
+
PEP 751 and pip's hash-checking mode each need at least one of
|
|
497
|
+
sha256/sha384/sha512 per artefact. The plain ``name==version``
|
|
498
|
+
writer records no hash and does not call this.
|
|
499
|
+
"""
|
|
500
|
+
from ..lockfile import ACCEPTED_HASH_ALGORITHMS, IndexPin
|
|
501
|
+
|
|
502
|
+
pin_groups: Iterable[Mapping[str, PinShape]] = (
|
|
503
|
+
lock_input.pins,
|
|
504
|
+
*lock_input.per_tuple_pins.values(),
|
|
505
|
+
)
|
|
506
|
+
for pins in pin_groups:
|
|
507
|
+
for pin in pins.values():
|
|
508
|
+
if not isinstance(pin, IndexPin):
|
|
509
|
+
continue
|
|
510
|
+
artefacts = (*pin.wheels, *((pin.sdist,) if pin.sdist is not None else ()))
|
|
511
|
+
for artefact in artefacts:
|
|
512
|
+
if not artefact.hashes:
|
|
513
|
+
msg = (
|
|
514
|
+
f"{pin.name}: artefact {artefact.filename!r} has no "
|
|
515
|
+
f"acceptable hash (need one of "
|
|
516
|
+
f"{list(ACCEPTED_HASH_ALGORITHMS)!r})"
|
|
517
|
+
)
|
|
518
|
+
raise MissingHashError(msg)
|
|
476
519
|
|
|
477
520
|
|
|
478
521
|
def _common_requires_python(files: Iterable[WheelFile | SdistFile]) -> str | None:
|
|
@@ -538,7 +581,7 @@ def _vcs_pin_from_source(
|
|
|
538
581
|
bare_repo_url = _strip_userinfo(parsed.repo_url)
|
|
539
582
|
repo_url = f"{parsed.scheme}+{bare_repo_url}@{resolved_sha}"
|
|
540
583
|
if parsed.subdirectory:
|
|
541
|
-
repo_url += f"#subdirectory={parsed.subdirectory}"
|
|
584
|
+
repo_url += f"#subdirectory={quote(parsed.subdirectory, safe='/')}"
|
|
542
585
|
|
|
543
586
|
return VcsPin(
|
|
544
587
|
name=canonical,
|
|
@@ -99,8 +99,8 @@ def validate_marker_disjointness(
|
|
|
99
99
|
|
|
100
100
|
``declared_groups`` carries every conflict set regardless of policy
|
|
101
101
|
so the hint can distinguish an undeclared collision (suggest adding
|
|
102
|
-
a declaration) from one already declared under ``
|
|
103
|
-
(suggest tightening to ``
|
|
102
|
+
a declaration) from one already declared under ``at-least-one``
|
|
103
|
+
(suggest tightening to ``at-most-one`` or ``exactly-one``).
|
|
104
104
|
"""
|
|
105
105
|
if not environments:
|
|
106
106
|
return
|
|
@@ -265,7 +265,7 @@ def _conflict_hint(
|
|
|
265
265
|
because a conflict declaration would not help.
|
|
266
266
|
|
|
267
267
|
When ``declared_groups`` already covers the active members, the
|
|
268
|
-
hint instead suggests tightening the policy: an ``
|
|
268
|
+
hint instead suggests tightening the policy: an ``at-least-one``
|
|
269
269
|
declaration permits co-selection, so the validator still raises and
|
|
270
270
|
the user has to switch to an exclusive policy to prune the point.
|
|
271
271
|
"""
|
|
@@ -288,8 +288,8 @@ def _conflict_hint(
|
|
|
288
288
|
if already_declared:
|
|
289
289
|
return (
|
|
290
290
|
". These members are declared in [tool.nab].conflicts under a"
|
|
291
|
-
" policy that permits co-selection; switch to
|
|
292
|
-
"
|
|
291
|
+
" policy that permits co-selection; switch to at-most-one or"
|
|
292
|
+
" exactly-one to prune the colliding context"
|
|
293
293
|
)
|
|
294
294
|
return (
|
|
295
295
|
". If these are intentionally mutually exclusive, declare them in"
|
nab_python/_lockfile/pylock.py
CHANGED
|
@@ -31,6 +31,7 @@ from .._vendor.packaging.specifiers import SpecifierSet
|
|
|
31
31
|
from .._vendor.packaging.utils import canonicalize_name
|
|
32
32
|
from .._vendor.packaging.version import Version
|
|
33
33
|
from ..config import conflict_exclusion_groups, conflict_member_groups
|
|
34
|
+
from .builder import require_artifact_hashes
|
|
34
35
|
from .disjointness import validate_marker_disjointness
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
@@ -82,14 +83,26 @@ def write_lock(
|
|
|
82
83
|
With no ``output_path`` the current directory is the base.
|
|
83
84
|
"""
|
|
84
85
|
lock_dir = Path(output_path).parent if output_path is not None else None
|
|
85
|
-
|
|
86
|
-
pylock.validate()
|
|
87
|
-
text = tomli_w.dumps(dict(pylock.to_dict()))
|
|
86
|
+
text = render_lock(lock_input, lock_dir=lock_dir)
|
|
88
87
|
if output_path is not None:
|
|
89
88
|
Path(output_path).write_text(text, encoding="utf-8")
|
|
90
89
|
return text
|
|
91
90
|
|
|
92
91
|
|
|
92
|
+
def render_lock(lock_input: LockInput, *, lock_dir: Path | None = None) -> str:
|
|
93
|
+
"""Serialise ``lock_input`` to PEP 751 TOML text without writing a file.
|
|
94
|
+
|
|
95
|
+
``lock_dir`` is the directory the lock will live in; directory, wheel
|
|
96
|
+
and sdist paths are emitted relative to it so the lock stays portable.
|
|
97
|
+
Defaults to the current directory. Used by ``write_lock`` and by
|
|
98
|
+
``nab lock --locked`` to render the would-be lock for comparison.
|
|
99
|
+
"""
|
|
100
|
+
require_artifact_hashes(lock_input)
|
|
101
|
+
pylock = build_pylock(lock_input, lock_dir=lock_dir)
|
|
102
|
+
pylock.validate()
|
|
103
|
+
return tomli_w.dumps(dict(pylock.to_dict()))
|
|
104
|
+
|
|
105
|
+
|
|
93
106
|
def build_pylock(lock_input: LockInput, *, lock_dir: Path | None = None) -> Pylock:
|
|
94
107
|
"""Build a :class:`Pylock` from the input shape.
|
|
95
108
|
|
|
@@ -539,7 +552,7 @@ def _build_marker(
|
|
|
539
552
|
``tuple_markers`` means the caller has not declared a tuple
|
|
540
553
|
universe and the marker is omitted.
|
|
541
554
|
|
|
542
|
-
A dep required by every member of an ``
|
|
555
|
+
A dep required by every member of an ``at-most-one`` set but not
|
|
543
556
|
by the base is absent from ``env_base_names``, so it keeps the
|
|
544
557
|
membership OR and does not install when no member is selected.
|
|
545
558
|
An environment with no base-name set (no conflict fork ran) leaves
|
|
@@ -11,6 +11,9 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
from .builder import require_artifact_hashes
|
|
14
17
|
|
|
15
18
|
if TYPE_CHECKING:
|
|
16
19
|
import os
|
|
@@ -38,6 +41,7 @@ def write_requirements_with_hashes(
|
|
|
38
41
|
``#subdirectory=`` fragment. Returns the text and, when
|
|
39
42
|
``output_path`` is provided, atomically writes it.
|
|
40
43
|
"""
|
|
44
|
+
require_artifact_hashes(lock_input)
|
|
41
45
|
return _render_requirements(lock_input, with_hashes=True, output_path=output_path)
|
|
42
46
|
|
|
43
47
|
|
|
@@ -101,7 +105,7 @@ def _render_pins(pins: Mapping[str, PinShape], *, with_hashes: bool) -> list[str
|
|
|
101
105
|
elif isinstance(pin, LocalPin):
|
|
102
106
|
url = Path(pin.path).resolve().as_uri()
|
|
103
107
|
if pin.subdirectory is not None:
|
|
104
|
-
url += f"#subdirectory={pin.subdirectory}"
|
|
108
|
+
url += f"#subdirectory={quote(pin.subdirectory, safe='/')}"
|
|
105
109
|
if pin.editable:
|
|
106
110
|
lines.append(f"-e {url}")
|
|
107
111
|
else:
|
|
@@ -22,12 +22,13 @@ from typing import TYPE_CHECKING
|
|
|
22
22
|
|
|
23
23
|
from nab_index.client import SdistFile, WheelFile, extract_sdist_archive
|
|
24
24
|
|
|
25
|
+
from .._vendor.packaging.specifiers import SpecifierSet
|
|
25
26
|
from .._vendor.packaging.utils import canonicalize_name
|
|
27
|
+
from .._vendor.packaging.version import Version
|
|
26
28
|
|
|
27
29
|
if TYPE_CHECKING:
|
|
28
30
|
from collections.abc import Sequence
|
|
29
31
|
|
|
30
|
-
from .._vendor.packaging.version import Version
|
|
31
32
|
from ..metadata import WheelMetadata
|
|
32
33
|
from ..provider import Provider
|
|
33
34
|
|
|
@@ -40,7 +41,14 @@ def build_remote_sdist(
|
|
|
40
41
|
"""Download the sdist for ``(package, version)``, extract, and build.
|
|
41
42
|
|
|
42
43
|
``package`` is the canonical package name; ``version`` matches an
|
|
43
|
-
entry in ``provider.versions_cache``.
|
|
44
|
+
entry in ``provider.versions_cache``. A built sdist whose
|
|
45
|
+
``Requires-Python`` excludes the resolve target is rejected, since the
|
|
46
|
+
Simple-API listing filter only sees the listing's own (possibly absent)
|
|
47
|
+
Requires-Python, not the value the build produces. A per-package
|
|
48
|
+
``requires-python`` metadata override substitutes for the built value
|
|
49
|
+
here, matching the listing gate. A built sdist whose declared name or
|
|
50
|
+
version disagrees with the requested candidate is also rejected rather
|
|
51
|
+
than used for the wrong package.
|
|
44
52
|
"""
|
|
45
53
|
# Late imports: ``provider`` imports this module at module load.
|
|
46
54
|
from .. import build_backend
|
|
@@ -58,8 +66,15 @@ def build_remote_sdist(
|
|
|
58
66
|
raise UnsupportedSdistError(msg)
|
|
59
67
|
|
|
60
68
|
ver_str = str(version)
|
|
61
|
-
event = provider.coordinator.request_sdist_archive(
|
|
69
|
+
event = provider.coordinator.request_sdist_archive(
|
|
70
|
+
canonical, ver_str, sdist.url, sdist.hashes
|
|
71
|
+
)
|
|
62
72
|
event.wait()
|
|
73
|
+
integrity_error = provider.coordinator.index.get_sdist_archive_error(
|
|
74
|
+
canonical, ver_str
|
|
75
|
+
)
|
|
76
|
+
if integrity_error is not None:
|
|
77
|
+
raise integrity_error
|
|
63
78
|
data = provider.coordinator.index.get_sdist_archive(canonical, ver_str)
|
|
64
79
|
if data is None:
|
|
65
80
|
msg = (
|
|
@@ -75,7 +90,7 @@ def build_remote_sdist(
|
|
|
75
90
|
msg = f"{package}=={version} sdist archive could not be extracted: {exc}"
|
|
76
91
|
raise UnsupportedSdistError(msg) from exc
|
|
77
92
|
try:
|
|
78
|
-
|
|
93
|
+
built = build_backend.extract_metadata(
|
|
79
94
|
source_dir,
|
|
80
95
|
config=provider.build_config,
|
|
81
96
|
python_version=provider.python_version,
|
|
@@ -84,6 +99,26 @@ def build_remote_sdist(
|
|
|
84
99
|
msg = f"{package}=={version} build-remote backend failed: {exc}"
|
|
85
100
|
raise UnsupportedSdistError(msg) from exc
|
|
86
101
|
|
|
102
|
+
target = provider.python_version
|
|
103
|
+
override_rp = provider.effective_requires_python(canonical, version)
|
|
104
|
+
spec = (
|
|
105
|
+
SpecifierSet(override_rp) if override_rp is not None else built.requires_python
|
|
106
|
+
)
|
|
107
|
+
if spec is not None and target and Version(target) not in spec:
|
|
108
|
+
msg = (
|
|
109
|
+
f"{package}=={version} built sdist requires Python {spec} but the"
|
|
110
|
+
f" resolve targets Python {target}"
|
|
111
|
+
)
|
|
112
|
+
raise UnsupportedSdistError(msg)
|
|
113
|
+
if canonicalize_name(built.name) != canonical or built.version != version:
|
|
114
|
+
msg = (
|
|
115
|
+
f"{package}=={version} built sdist declares"
|
|
116
|
+
f" {built.name}=={built.version}, which does not match the"
|
|
117
|
+
" requested candidate"
|
|
118
|
+
)
|
|
119
|
+
raise UnsupportedSdistError(msg)
|
|
120
|
+
return built
|
|
121
|
+
|
|
87
122
|
|
|
88
123
|
def _find_sdist(
|
|
89
124
|
versions: Sequence[tuple[Version, WheelFile | SdistFile]],
|
nab_python/_provider/extras.py
CHANGED
|
@@ -13,6 +13,7 @@ import logging
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
15
|
from .._vendor.packaging.ranges import VersionRange
|
|
16
|
+
from .metadata_resolver import refuse_url_dep
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from nab_resolver.types import RangeProtocol
|
|
@@ -187,6 +188,11 @@ def get_extra_dependencies(
|
|
|
187
188
|
msg = f"No metadata cached for {base}=={version}"
|
|
188
189
|
raise MetadataError(msg)
|
|
189
190
|
|
|
191
|
+
deferred = provider.deferred_url_extras.get(base_cache_key, {}).get(extra)
|
|
192
|
+
if deferred:
|
|
193
|
+
req, url = deferred[0]
|
|
194
|
+
refuse_url_dep(provider, req, url)
|
|
195
|
+
|
|
190
196
|
extra_map = provider.extra_deps_map.get(base_cache_key, {})
|
|
191
197
|
if extra not in extra_map:
|
|
192
198
|
return handle_missing_extra(provider, normalized, extra, version, cache_key)
|