nab-python 0.0.4__py3-none-any.whl → 0.0.5__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/_lockfile/pylock.py +66 -1
- nab_python/_lockfile/requirements.py +13 -5
- nab_python/_provider/extras.py +33 -24
- nab_python/_provider/listing.py +19 -12
- nab_python/_provider/metadata_resolver.py +5 -17
- nab_python/_provider/priority.py +3 -1
- nab_python/_vcs_admission.py +7 -6
- nab_python/download.py +4 -2
- nab_python/fetch.py +33 -6
- nab_python/lockfile.py +2 -1
- nab_python/metadata.py +24 -2
- nab_python/provider.py +11 -12
- nab_python/resolve.py +2 -4
- nab_python/universal/matrix.py +11 -4
- nab_python/universal/reresolve.py +11 -1
- nab_python/universal/validate.py +49 -11
- nab_python/universal/wheel_selection.py +22 -2
- {nab_python-0.0.4.dist-info → nab_python-0.0.5.dist-info}/METADATA +3 -3
- {nab_python-0.0.4.dist-info → nab_python-0.0.5.dist-info}/RECORD +20 -20
- {nab_python-0.0.4.dist-info → nab_python-0.0.5.dist-info}/WHEEL +0 -0
nab_python/_lockfile/pylock.py
CHANGED
|
@@ -45,11 +45,26 @@ if TYPE_CHECKING:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
__all__ = [
|
|
48
|
+
"DivergentBaseDependencyError",
|
|
48
49
|
"build_pylock",
|
|
49
50
|
"write_lock",
|
|
50
51
|
]
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
class DivergentBaseDependencyError(ValueError):
|
|
55
|
+
"""An environment's conflict forks disagree on a base dependency's pin.
|
|
56
|
+
|
|
57
|
+
A base dependency present in every fork of an environment drops its
|
|
58
|
+
membership clause so it installs even when no conflicting member is
|
|
59
|
+
selected, which requires the forks to agree on one (version, source).
|
|
60
|
+
When they diverge, every candidate entry keeps a membership clause,
|
|
61
|
+
so nothing would fire in the no-member install context and the
|
|
62
|
+
dependency would silently not install. Surface the divergence with
|
|
63
|
+
the offending package and per-fork pins so the producer can
|
|
64
|
+
reconcile the forks rather than commit an incomplete lock.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
53
68
|
def write_lock(
|
|
54
69
|
lock_input: LockInput,
|
|
55
70
|
*,
|
|
@@ -317,7 +332,9 @@ def _build_per_tuple_packages(lock_input: LockInput, lock_dir: Path) -> list[Pac
|
|
|
317
332
|
not by the base is absent from that set, so it keeps the
|
|
318
333
|
membership clause and does not install when no member is selected.
|
|
319
334
|
See :class:`LockInput.env_base_names` for the missing-signature
|
|
320
|
-
contract.
|
|
335
|
+
contract. Forks of one environment that disagree on a base
|
|
336
|
+
dependency's pin raise :class:`DivergentBaseDependencyError`
|
|
337
|
+
instead of emitting a lock whose no-member context misses it.
|
|
321
338
|
"""
|
|
322
339
|
out: list[Package] = []
|
|
323
340
|
by_name = _group_by_name(lock_input.per_tuple_pins)
|
|
@@ -330,6 +347,14 @@ def _build_per_tuple_packages(lock_input: LockInput, lock_dir: Path) -> list[Pac
|
|
|
330
347
|
)
|
|
331
348
|
for canonical_name, per_tuple in by_name.items():
|
|
332
349
|
groups = _group_pins_by_pin(per_tuple)
|
|
350
|
+
_check_base_fork_agreement(
|
|
351
|
+
canonical_name,
|
|
352
|
+
per_tuple,
|
|
353
|
+
groups,
|
|
354
|
+
env_signatures,
|
|
355
|
+
env_fork_counts,
|
|
356
|
+
lock_input.env_base_names,
|
|
357
|
+
)
|
|
333
358
|
for pins, tuple_labels in groups:
|
|
334
359
|
marker = _build_marker(
|
|
335
360
|
canonical_name,
|
|
@@ -452,6 +477,46 @@ def _merge_pins_in_group(pins: list[PinShape]) -> PinShape:
|
|
|
452
477
|
)
|
|
453
478
|
|
|
454
479
|
|
|
480
|
+
def _check_base_fork_agreement(
|
|
481
|
+
name: str,
|
|
482
|
+
per_tuple: Mapping[str, PinShape],
|
|
483
|
+
groups: list[tuple[list[PinShape], list[str]]],
|
|
484
|
+
env_signatures: Mapping[str, tuple[tuple[str, str], ...]],
|
|
485
|
+
env_fork_counts: Mapping[tuple[tuple[str, str], ...], int],
|
|
486
|
+
env_base_names: Mapping[tuple[tuple[str, str], ...], frozenset[str]],
|
|
487
|
+
) -> None:
|
|
488
|
+
"""Reject a base dep whose forks within one env pin it differently.
|
|
489
|
+
|
|
490
|
+
The env-only collapse in :func:`_build_marker` needs a single
|
|
491
|
+
(version, source) group spanning every fork of the environment.
|
|
492
|
+
Divergent pins split the forks across groups, so every entry would
|
|
493
|
+
keep its membership clause and none would fire when no member is
|
|
494
|
+
selected: the base dependency would silently not install.
|
|
495
|
+
"""
|
|
496
|
+
by_env: defaultdict[tuple[tuple[str, str], ...], list[str]] = defaultdict(list)
|
|
497
|
+
for label in sorted(per_tuple.keys() & env_signatures.keys()):
|
|
498
|
+
by_env[env_signatures[label]].append(label)
|
|
499
|
+
for signature, labels in by_env.items():
|
|
500
|
+
if len(labels) < env_fork_counts[signature]:
|
|
501
|
+
continue
|
|
502
|
+
if name not in env_base_names.get(signature, frozenset()):
|
|
503
|
+
continue
|
|
504
|
+
in_env = set(labels)
|
|
505
|
+
widest = max(
|
|
506
|
+
sum(1 for label in group_labels if label in in_env)
|
|
507
|
+
for _, group_labels in groups
|
|
508
|
+
)
|
|
509
|
+
if widest >= env_fork_counts[signature]:
|
|
510
|
+
continue
|
|
511
|
+
forks = ", ".join(f"{label} -> {per_tuple[label].version}" for label in labels)
|
|
512
|
+
msg = (
|
|
513
|
+
f"{name}: the conflict forks of one environment pin this base"
|
|
514
|
+
f" dependency differently ({forks}); no lockfile entry would"
|
|
515
|
+
" install it when no conflicting member is selected"
|
|
516
|
+
)
|
|
517
|
+
raise DivergentBaseDependencyError(msg)
|
|
518
|
+
|
|
519
|
+
|
|
455
520
|
def _build_marker(
|
|
456
521
|
name: str,
|
|
457
522
|
tuple_labels: Sequence[str],
|
|
@@ -33,8 +33,10 @@ def write_requirements_with_hashes(
|
|
|
33
33
|
Each line is ``name==version`` followed by one ``--hash=sha256:...``
|
|
34
34
|
per recorded artefact, in the format pip's hash-checking mode
|
|
35
35
|
accepts. Local and VCS pins are emitted as ``name @ <url>`` lines
|
|
36
|
-
without hashes (pip does not hash-check those forms)
|
|
37
|
-
|
|
36
|
+
without hashes (pip does not hash-check those forms); an editable
|
|
37
|
+
local pin renders as ``-e <url>`` and a ``subdirectory`` as a
|
|
38
|
+
``#subdirectory=`` fragment. Returns the text and, when
|
|
39
|
+
``output_path`` is provided, atomically writes it.
|
|
38
40
|
"""
|
|
39
41
|
return _render_requirements(lock_input, with_hashes=True, output_path=output_path)
|
|
40
42
|
|
|
@@ -45,8 +47,8 @@ def write_requirements_without_hashes(
|
|
|
45
47
|
"""Render ``lock_input`` as a plain ``name==version`` list.
|
|
46
48
|
|
|
47
49
|
Same shape as :func:`write_requirements_with_hashes` but without
|
|
48
|
-
the ``--hash=sha256:...`` lines. Local and VCS pins
|
|
49
|
-
|
|
50
|
+
the ``--hash=sha256:...`` lines. Local and VCS pins render the
|
|
51
|
+
same in both variants. Returns the text and, when ``output_path``
|
|
50
52
|
is provided, atomically writes it.
|
|
51
53
|
"""
|
|
52
54
|
return _render_requirements(lock_input, with_hashes=False, output_path=output_path)
|
|
@@ -97,7 +99,13 @@ def _render_pins(pins: Mapping[str, PinShape], *, with_hashes: bool) -> list[str
|
|
|
97
99
|
if isinstance(pin, IndexPin):
|
|
98
100
|
lines.extend(_render_index_pin(pin, with_hashes=with_hashes))
|
|
99
101
|
elif isinstance(pin, LocalPin):
|
|
100
|
-
|
|
102
|
+
url = Path(pin.path).resolve().as_uri()
|
|
103
|
+
if pin.subdirectory is not None:
|
|
104
|
+
url += f"#subdirectory={pin.subdirectory}"
|
|
105
|
+
if pin.editable:
|
|
106
|
+
lines.append(f"-e {url}")
|
|
107
|
+
else:
|
|
108
|
+
lines.append(f"{pin.name} @ {url}")
|
|
101
109
|
elif isinstance(pin, VcsPin):
|
|
102
110
|
lines.append(f"{pin.name} @ {pin.repo_url}")
|
|
103
111
|
else: # pragma: no cover - exhaustive
|
nab_python/_provider/extras.py
CHANGED
|
@@ -76,17 +76,23 @@ def _pick_in_mode(
|
|
|
76
76
|
) -> Version | None:
|
|
77
77
|
"""Pick a candidate honoring ``ExtrasMode``.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Missing-metadata cases (no PEP 658, no sdist)
|
|
86
|
-
|
|
79
|
+
Fetches base metadata so an extraction failure (unparseable
|
|
80
|
+
PKG-INFO, or an sdist build the policy disallows) becomes a
|
|
81
|
+
candidate skip instead of a fatal error during the later
|
|
82
|
+
dependency fetch. BACKTRACK mode additionally checks
|
|
83
|
+
``Provides-Extra`` for transitive extras.
|
|
84
|
+
|
|
85
|
+
Missing-metadata cases (no PEP 658, no sdist) skip transitive
|
|
86
|
+
extras but fall through for user-requested ones; mock test
|
|
87
|
+
coordinators rely on this.
|
|
87
88
|
"""
|
|
88
89
|
# Late import: ``pypi`` imports this module at module load.
|
|
89
|
-
from ..provider import
|
|
90
|
+
from ..provider import (
|
|
91
|
+
ExtrasMode,
|
|
92
|
+
MetadataError,
|
|
93
|
+
UnsupportedSdistError,
|
|
94
|
+
_normalize_extra,
|
|
95
|
+
)
|
|
90
96
|
|
|
91
97
|
_, _, normalized = provider.split_and_normalize(base)
|
|
92
98
|
is_user = (normalized, extra) in provider.root_extras
|
|
@@ -94,17 +100,15 @@ def _pick_in_mode(
|
|
|
94
100
|
for version in candidates:
|
|
95
101
|
if provider.has_invalid_metadata(normalized, version):
|
|
96
102
|
continue
|
|
97
|
-
if is_user:
|
|
98
|
-
return version
|
|
99
|
-
# Fetch base metadata so an unparseable PKG-INFO is caught
|
|
100
|
-
# before the extras proxy decides this version. Any
|
|
101
|
-
# MetadataError (parse failure or no metadata source) is a
|
|
102
|
-
# candidate skip.
|
|
103
103
|
try:
|
|
104
104
|
provider.get_dependencies(base, version)
|
|
105
|
-
except
|
|
105
|
+
except UnsupportedSdistError:
|
|
106
106
|
continue
|
|
107
|
-
|
|
107
|
+
except MetadataError:
|
|
108
|
+
if not is_user or provider.has_invalid_metadata(normalized, version):
|
|
109
|
+
continue
|
|
110
|
+
return version
|
|
111
|
+
if is_user or not backtrack:
|
|
108
112
|
return version
|
|
109
113
|
metadata = provider.metadata_cache.get((normalized, version))
|
|
110
114
|
provided = (
|
|
@@ -188,7 +192,11 @@ def get_extra_dependencies(
|
|
|
188
192
|
return handle_missing_extra(provider, normalized, extra, version, cache_key)
|
|
189
193
|
|
|
190
194
|
deps = dict(extra_map[extra])
|
|
191
|
-
|
|
195
|
+
# Pin the base, intersected with any bound the extra itself
|
|
196
|
+
# places on it (``foo>=2; extra == "bar"``).
|
|
197
|
+
deps[normalized] = deps.get(
|
|
198
|
+
normalized, VersionRange.full()
|
|
199
|
+
) & VersionRange.singleton(version)
|
|
192
200
|
|
|
193
201
|
provider.deps_cache[cache_key] = deps
|
|
194
202
|
provider.prefetch_new_deps(deps)
|
|
@@ -223,9 +231,10 @@ def handle_missing_extra(
|
|
|
223
231
|
version,
|
|
224
232
|
extra,
|
|
225
233
|
)
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
# The extra contributes no deps at this version, but the proxy
|
|
235
|
+
# must still pin its base: without the pin the proxy and the base
|
|
236
|
+
# can settle on different versions, and if the base's version does
|
|
237
|
+
# provide the extra its dependencies are silently dropped.
|
|
238
|
+
deps = {normalized: VersionRange.singleton(version)}
|
|
239
|
+
provider.deps_cache[cache_key] = deps
|
|
240
|
+
return deps
|
nab_python/_provider/listing.py
CHANGED
|
@@ -271,10 +271,10 @@ def excluded_by_python(provider: Provider, dist: DistFile) -> bool:
|
|
|
271
271
|
try:
|
|
272
272
|
spec = SpecifierSet(requires_python)
|
|
273
273
|
cached = Version(provider.python_version) not in spec
|
|
274
|
-
except
|
|
275
|
-
# Malformed Requires-Python on the dist
|
|
276
|
-
#
|
|
277
|
-
#
|
|
274
|
+
except InvalidSpecifier:
|
|
275
|
+
# Malformed Requires-Python on the dist: treat as
|
|
276
|
+
# not-excluded, let downstream logic decide. Our own
|
|
277
|
+
# python_version is validated at Provider construction.
|
|
278
278
|
cached = False
|
|
279
279
|
provider.requires_python_cache[requires_python] = cached
|
|
280
280
|
if cached:
|
|
@@ -425,14 +425,21 @@ def await_metadata_batch(
|
|
|
425
425
|
event.wait()
|
|
426
426
|
text = provider.coordinator.index.get_metadata(package, ver_str)
|
|
427
427
|
if text is None:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
428
|
+
# No PEP 658 text arrived: leave the version un-cached so
|
|
429
|
+
# look-ahead's get_dependencies runs the sdist fallback (or
|
|
430
|
+
# refuses it) rather than pinning it as dependency-free.
|
|
431
|
+
continue
|
|
432
|
+
if provider.coordinator.index.metadata_from_sdist(package, ver_str):
|
|
433
|
+
# The shared slot holds sdist PKG-INFO from an earlier
|
|
434
|
+
# fallback; caching it here would skip the PEP 643 gate
|
|
435
|
+
# that get_dependencies applies on the from_sdist path.
|
|
436
|
+
continue
|
|
437
|
+
try:
|
|
438
|
+
provider.parse_and_cache_metadata(cache_key, text)
|
|
439
|
+
except (ValueError, InvalidVersion, InvalidSpecifier):
|
|
440
|
+
# Malformed metadata: same reason, refuse via get_dependencies
|
|
441
|
+
# (_invalid_metadata) instead of caching empty deps.
|
|
442
|
+
continue
|
|
436
443
|
|
|
437
444
|
|
|
438
445
|
def prefetch_new_deps(provider: Provider, deps: Mapping[str, VersionRange]) -> None:
|
|
@@ -48,11 +48,11 @@ def resolve_metadata(
|
|
|
48
48
|
|
|
49
49
|
text = provider.coordinator.index.get_metadata(normalized, ver_str)
|
|
50
50
|
if text is not None:
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
from_sdist =
|
|
51
|
+
# Wheel METADATA and sdist PKG-INFO share the slot; the index
|
|
52
|
+
# records which kind the last write was. Inferring from the
|
|
53
|
+
# listing instead would mislabel the text whenever this
|
|
54
|
+
# provider's view differs from the one that stored it.
|
|
55
|
+
from_sdist = provider.coordinator.index.metadata_from_sdist(normalized, ver_str)
|
|
56
56
|
return (text, from_sdist)
|
|
57
57
|
|
|
58
58
|
dist = pick_dist_for_metadata(versions, version)
|
|
@@ -85,18 +85,6 @@ def resolve_metadata(
|
|
|
85
85
|
raise MetadataError(msg)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def has_wheel_metadata_at(
|
|
89
|
-
versions: Sequence[tuple[Version, DistFile]], version: Version
|
|
90
|
-
) -> bool:
|
|
91
|
-
"""Report whether the listing has a wheel with PEP 658 metadata."""
|
|
92
|
-
for v, d in versions:
|
|
93
|
-
if v != version:
|
|
94
|
-
continue
|
|
95
|
-
if isinstance(d, WheelFile) and d.metadata_url is not None:
|
|
96
|
-
return True
|
|
97
|
-
return False
|
|
98
|
-
|
|
99
|
-
|
|
100
88
|
def pick_dist_for_metadata(
|
|
101
89
|
versions: Sequence[tuple[Version, DistFile]], version: Version
|
|
102
90
|
) -> DistFile | None:
|
nab_python/_provider/priority.py
CHANGED
|
@@ -93,7 +93,9 @@ def compute_matching(
|
|
|
93
93
|
elif has_local_source:
|
|
94
94
|
matching = 1
|
|
95
95
|
else:
|
|
96
|
-
|
|
96
|
+
# Not cached, so the next call re-checks the index and the
|
|
97
|
+
# listing-arrival side effect above can still fire.
|
|
98
|
+
return _NO_LISTING_PRIOR
|
|
97
99
|
|
|
98
100
|
if per_pkg is None:
|
|
99
101
|
per_pkg = provider.matching_cache[normalized] = {}
|
nab_python/_vcs_admission.py
CHANGED
|
@@ -103,15 +103,16 @@ def split_vcs_scheme(url: str) -> tuple[str | None, str]:
|
|
|
103
103
|
def has_full_commit_sha(url: str) -> bool:
|
|
104
104
|
"""Return True if the URL pins to a 40-char hex commit hash.
|
|
105
105
|
|
|
106
|
-
Looks for ``@<sha>``
|
|
107
|
-
``#`` fragment.
|
|
108
|
-
|
|
106
|
+
Looks for ``@<sha>`` in the path component (after the authority);
|
|
107
|
+
ignores any ``#`` fragment. A ``user@host`` in the authority is
|
|
108
|
+
left alone, matching the ref parsing in :mod:`nab_index.vcs`.
|
|
109
109
|
"""
|
|
110
110
|
fragmentless = url.split("#", 1)[0]
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
after_scheme = fragmentless.split("://", 1)[-1]
|
|
112
|
+
path = after_scheme.partition("/")[2]
|
|
113
|
+
if "@" not in path:
|
|
113
114
|
return False
|
|
114
|
-
ref =
|
|
115
|
+
ref = path.rsplit("@", 1)[1]
|
|
115
116
|
return bool(FULL_GIT_SHA_RE.match(ref))
|
|
116
117
|
|
|
117
118
|
|
nab_python/download.py
CHANGED
|
@@ -104,6 +104,8 @@ def _entries_for_pin(canonical: str, pin: PinShape) -> Iterable[DownloadEntry]:
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
107
|
+
# Recorded digests are lowercased to match hashlib.hexdigest() output:
|
|
108
|
+
# index-fed flows already lowercase, but a caller-built LockInput may not.
|
|
107
109
|
if pin.sdist is not None:
|
|
108
110
|
algo, digest = pin.sdist.primary_digest
|
|
109
111
|
yield DownloadEntry(
|
|
@@ -112,7 +114,7 @@ def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
|
112
114
|
filename=pin.sdist.filename,
|
|
113
115
|
url=pin.sdist.url,
|
|
114
116
|
hash_algo=algo,
|
|
115
|
-
digest=digest,
|
|
117
|
+
digest=digest.lower(),
|
|
116
118
|
)
|
|
117
119
|
for wheel in pin.wheels:
|
|
118
120
|
algo, digest = wheel.primary_digest
|
|
@@ -122,7 +124,7 @@ def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
|
122
124
|
filename=wheel.filename,
|
|
123
125
|
url=wheel.url,
|
|
124
126
|
hash_algo=algo,
|
|
125
|
-
digest=digest,
|
|
127
|
+
digest=digest.lower(),
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
|
nab_python/fetch.py
CHANGED
|
@@ -141,6 +141,10 @@ class InMemoryIndex:
|
|
|
141
141
|
self._listing_errors: dict[str, BaseException] = {}
|
|
142
142
|
self._listing_indexes: dict[str, str] = {}
|
|
143
143
|
self._metadata: dict[tuple[str, str], str | None] = {}
|
|
144
|
+
# Keys whose ``_metadata`` slot was last written from an sdist
|
|
145
|
+
# PKG-INFO rather than a wheel METADATA; readers need the
|
|
146
|
+
# origin because only sdist deps go through the PEP 643 gate.
|
|
147
|
+
self._metadata_from_sdist: set[tuple[str, str]] = set()
|
|
144
148
|
self._sdist_pyproject: dict[tuple[str, str], str | None] = {}
|
|
145
149
|
self._sdist_archives: dict[tuple[str, str], bytes | None] = {}
|
|
146
150
|
self._pending: dict[str, _Pending] = {}
|
|
@@ -227,6 +231,7 @@ class InMemoryIndex:
|
|
|
227
231
|
key = f"metadata:{package}:{version}"
|
|
228
232
|
with self._lock:
|
|
229
233
|
self._metadata[(package, version)] = data
|
|
234
|
+
self._metadata_from_sdist.discard((package, version))
|
|
230
235
|
pending = self._pending.get(key)
|
|
231
236
|
if pending is not None:
|
|
232
237
|
pending.result = data
|
|
@@ -241,15 +246,27 @@ class InMemoryIndex:
|
|
|
241
246
|
because PKG-INFO is core-metadata-equivalent. The pending
|
|
242
247
|
keys differ so a sdist request can run in parallel with (or
|
|
243
248
|
after) a failed wheel metadata request.
|
|
249
|
+
:meth:`metadata_from_sdist` reports which kind the slot holds.
|
|
244
250
|
"""
|
|
245
251
|
key = f"sdist:{package}:{version}"
|
|
246
252
|
with self._lock:
|
|
247
253
|
self._metadata[(package, version)] = data
|
|
254
|
+
self._metadata_from_sdist.add((package, version))
|
|
248
255
|
pending = self._pending.get(key)
|
|
249
256
|
if pending is not None:
|
|
250
257
|
pending.result = data
|
|
251
258
|
pending.event.set()
|
|
252
259
|
|
|
260
|
+
def metadata_from_sdist(self, package: str, version: str) -> bool:
|
|
261
|
+
"""Return ``True`` when the metadata slot was last written from an sdist.
|
|
262
|
+
|
|
263
|
+
The slot itself cannot distinguish wheel METADATA from sdist
|
|
264
|
+
PKG-INFO; readers that apply the :pep:`643` dynamic-deps gate
|
|
265
|
+
only to sdist values ask here for the current text's origin.
|
|
266
|
+
"""
|
|
267
|
+
with self._lock:
|
|
268
|
+
return (package, version) in self._metadata_from_sdist
|
|
269
|
+
|
|
253
270
|
def store_sdist_pyproject(self, package: str, version: str, data: str) -> None:
|
|
254
271
|
"""Store sdist-derived pyproject.toml text for static-metadata fallback.
|
|
255
272
|
|
|
@@ -470,6 +487,11 @@ class FetchCoordinator:
|
|
|
470
487
|
self._thread.join(timeout=_COORDINATOR_JOIN_TIMEOUT_SECONDS)
|
|
471
488
|
self._thread = None
|
|
472
489
|
self._started = False
|
|
490
|
+
# Drop the dead loop so a later start() waits for the fresh one
|
|
491
|
+
# instead of submitting to a closed loop.
|
|
492
|
+
self._loop = None
|
|
493
|
+
self._async_q = None
|
|
494
|
+
self._queue_ready.clear()
|
|
473
495
|
|
|
474
496
|
def _submit(self, item: _QueueItem) -> None:
|
|
475
497
|
"""Schedule ``item`` on the fetcher loop's queue from any thread."""
|
|
@@ -711,7 +733,8 @@ class FetchCoordinator:
|
|
|
711
733
|
|
|
712
734
|
client = self._build_client()
|
|
713
735
|
try:
|
|
714
|
-
|
|
736
|
+
stopping = False
|
|
737
|
+
while not stopping:
|
|
715
738
|
item = await queue.get()
|
|
716
739
|
if item is None:
|
|
717
740
|
break
|
|
@@ -725,10 +748,11 @@ class FetchCoordinator:
|
|
|
725
748
|
except asyncio.QueueEmpty:
|
|
726
749
|
break
|
|
727
750
|
if extra is None:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
751
|
+
# Fall through to the gather below instead of
|
|
752
|
+
# cancelling: a cancelled _handle never records a
|
|
753
|
+
# result, leaving its waiter's event unset forever.
|
|
754
|
+
stopping = True
|
|
755
|
+
break
|
|
732
756
|
self._dispatch(extra, client, sem, tasks)
|
|
733
757
|
|
|
734
758
|
if tasks:
|
|
@@ -856,9 +880,12 @@ class FetchCoordinator:
|
|
|
856
880
|
pkg_info, pyproject = await client.get_sdist_files(
|
|
857
881
|
req.package, req.version, req.url
|
|
858
882
|
)
|
|
859
|
-
|
|
883
|
+
# Store pyproject.toml first: store_sdist_metadata fires the
|
|
884
|
+
# pending event, and a released waiter reads the pyproject slot
|
|
885
|
+
# with no further synchronisation.
|
|
860
886
|
if pyproject is not None:
|
|
861
887
|
self.index.store_sdist_pyproject(req.package, req.version, pyproject)
|
|
888
|
+
self.index.store_sdist_metadata(req.package, req.version, pkg_info)
|
|
862
889
|
|
|
863
890
|
async def _fetch_sdist_archive(
|
|
864
891
|
self,
|
nab_python/lockfile.py
CHANGED
|
@@ -24,7 +24,7 @@ from ._lockfile.builder import (
|
|
|
24
24
|
read_lockfile_packages,
|
|
25
25
|
)
|
|
26
26
|
from ._lockfile.disjointness import DisjointnessError
|
|
27
|
-
from ._lockfile.pylock import build_pylock, write_lock
|
|
27
|
+
from ._lockfile.pylock import DivergentBaseDependencyError, build_pylock, write_lock
|
|
28
28
|
from ._lockfile.requirements import (
|
|
29
29
|
write_requirements_with_hashes,
|
|
30
30
|
write_requirements_without_hashes,
|
|
@@ -44,6 +44,7 @@ __all__ = [
|
|
|
44
44
|
"ACCEPTED_HASH_ALGORITHMS",
|
|
45
45
|
"LOCK_VERSION",
|
|
46
46
|
"DisjointnessError",
|
|
47
|
+
"DivergentBaseDependencyError",
|
|
47
48
|
"IndexPin",
|
|
48
49
|
"LocalPin",
|
|
49
50
|
"LockInput",
|
nab_python/metadata.py
CHANGED
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import email.parser
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from functools import lru_cache
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
17
|
import tomli
|
|
18
18
|
|
|
@@ -20,6 +20,9 @@ from ._vendor.packaging.requirements import Requirement
|
|
|
20
20
|
from ._vendor.packaging.specifiers import SpecifierSet
|
|
21
21
|
from ._vendor.packaging.version import Version
|
|
22
22
|
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ._vendor.packaging.markers import Marker
|
|
25
|
+
|
|
23
26
|
__all__ = [
|
|
24
27
|
"DEPENDENCY_FIELDS",
|
|
25
28
|
"WheelMetadata",
|
|
@@ -88,6 +91,22 @@ def metadata_deps_are_static(metadata: WheelMetadata) -> bool:
|
|
|
88
91
|
return not (DEPENDENCY_FIELDS & metadata.dynamic)
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
@lru_cache(maxsize=8192)
|
|
95
|
+
def _intern_marker(marker: Marker) -> Marker:
|
|
96
|
+
"""Return a shared :class:`Marker` for an equal marker expression.
|
|
97
|
+
|
|
98
|
+
``Marker`` hashes and compares by its text, so a single marker like
|
|
99
|
+
``extra == "test"`` recurs across hundreds of distinct dep strings
|
|
100
|
+
(``pytest; extra == "test"``, ``coverage; extra == "test"``, ...),
|
|
101
|
+
each parsing to its own object. The provider caches marker
|
|
102
|
+
evaluation by ``id(marker)``, so sharing one object per distinct
|
|
103
|
+
expression lets that cache hit across every candidate instead of
|
|
104
|
+
re-evaluating the same expression per dep. Markers are read-only,
|
|
105
|
+
so sharing is safe.
|
|
106
|
+
"""
|
|
107
|
+
return marker
|
|
108
|
+
|
|
109
|
+
|
|
91
110
|
@lru_cache(maxsize=16384)
|
|
92
111
|
def _parse_requirement_cached(req_str: str) -> Requirement:
|
|
93
112
|
"""Cache ``Requirement(req_str)`` parsing across wheel metadata.
|
|
@@ -97,7 +116,10 @@ def _parse_requirement_cached(req_str: str) -> Requirement:
|
|
|
97
116
|
only read operations (specifier, marker, extras, name) so sharing
|
|
98
117
|
parsed objects is safe.
|
|
99
118
|
"""
|
|
100
|
-
|
|
119
|
+
req = Requirement(req_str)
|
|
120
|
+
if req.marker is not None:
|
|
121
|
+
req.marker = _intern_marker(req.marker)
|
|
122
|
+
return req
|
|
101
123
|
|
|
102
124
|
|
|
103
125
|
@lru_cache(maxsize=65536)
|
nab_python/provider.py
CHANGED
|
@@ -7,7 +7,6 @@ types. Uses a thread pool with a shared HTTP session to overlap I/O.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import contextlib
|
|
11
10
|
import enum
|
|
12
11
|
import logging
|
|
13
12
|
import re
|
|
@@ -98,16 +97,17 @@ _PYTHON_FULL_VERSION_PARTS = 3
|
|
|
98
97
|
def python_axis_environment(python_version: str) -> dict[str, str]:
|
|
99
98
|
"""Map an explicit Python version to its PEP 508 marker keys.
|
|
100
99
|
|
|
101
|
-
``
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
``python_version`` is padded to two components and
|
|
101
|
+
``python_full_version`` to three so patch-precision markers evaluate
|
|
102
|
+
the same here as in the universal matrix. Raises ``InvalidVersion``
|
|
103
|
+
if the input is not a version.
|
|
104
104
|
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
105
|
+
try:
|
|
106
|
+
release = Version(python_version).release
|
|
107
|
+
except InvalidVersion:
|
|
108
|
+
msg = f"python_version {python_version!r} is not a valid version"
|
|
109
|
+
raise InvalidVersion(msg) from None
|
|
110
|
+
minor = ".".join(str(part) for part in (*release, 0)[:_PYTHON_VERSION_PARTS])
|
|
111
111
|
full = (
|
|
112
112
|
python_version
|
|
113
113
|
if len(release) >= _PYTHON_FULL_VERSION_PARTS
|
|
@@ -494,8 +494,7 @@ class Provider:
|
|
|
494
494
|
}
|
|
495
495
|
self.environment: dict[str, str] = env_init
|
|
496
496
|
if python_version is not None:
|
|
497
|
-
|
|
498
|
-
self.environment.update(python_axis_environment(python_version))
|
|
497
|
+
self.environment.update(python_axis_environment(python_version))
|
|
499
498
|
if marker_environment:
|
|
500
499
|
for key, value in marker_environment.items():
|
|
501
500
|
self.environment[key] = value
|
nab_python/resolve.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import contextlib
|
|
6
5
|
import itertools
|
|
7
6
|
import logging
|
|
8
7
|
import sys
|
|
@@ -23,7 +22,7 @@ from ._vendor.packaging.ranges import VersionRange
|
|
|
23
22
|
from ._vendor.packaging.requirements import Requirement
|
|
24
23
|
from ._vendor.packaging.specifiers import SpecifierSet
|
|
25
24
|
from ._vendor.packaging.utils import canonicalize_name
|
|
26
|
-
from ._vendor.packaging.version import
|
|
25
|
+
from ._vendor.packaging.version import Version
|
|
27
26
|
from .config import (
|
|
28
27
|
ConfigError,
|
|
29
28
|
ConflictFork,
|
|
@@ -689,8 +688,7 @@ def _build_marker_environment(
|
|
|
689
688
|
for key, value in default_environment().items()
|
|
690
689
|
if isinstance(value, str)
|
|
691
690
|
}
|
|
692
|
-
|
|
693
|
-
env.update(python_axis_environment(python_version))
|
|
691
|
+
env.update(python_axis_environment(python_version))
|
|
694
692
|
env.update(overrides)
|
|
695
693
|
return env
|
|
696
694
|
|
nab_python/universal/matrix.py
CHANGED
|
@@ -231,9 +231,9 @@ class Matrix:
|
|
|
231
231
|
"""Expand the matrix into concrete tuples.
|
|
232
232
|
|
|
233
233
|
Validates inputs eagerly: unknown platform ids, unknown
|
|
234
|
-
implementations,
|
|
235
|
-
|
|
236
|
-
happens.
|
|
234
|
+
implementations, ``python_patches`` keys that are not known
|
|
235
|
+
minors, an empty python range, or an invalid ``python_order``
|
|
236
|
+
each raise a ``ValueError`` before any work happens.
|
|
237
237
|
|
|
238
238
|
``platforms`` accepts either bare platform-id strings (use
|
|
239
239
|
default tag floors) or :class:`PlatformSpec` instances for
|
|
@@ -258,13 +258,20 @@ class Matrix:
|
|
|
258
258
|
if unknown_impl:
|
|
259
259
|
msg = f"Unknown implementations: {unknown_impl!r}"
|
|
260
260
|
raise ValueError(msg)
|
|
261
|
+
patches = self.python_patches or {}
|
|
262
|
+
unknown_patches = [m for m in patches if m not in _KNOWN_PYTHON_MINORS]
|
|
263
|
+
if unknown_patches:
|
|
264
|
+
msg = (
|
|
265
|
+
f"Unknown python_patches minors: {unknown_patches!r};"
|
|
266
|
+
" keys must be major.minor like '3.11'"
|
|
267
|
+
)
|
|
268
|
+
raise ValueError(msg)
|
|
261
269
|
py_versions = list(_pythons_in_range(self.python))
|
|
262
270
|
if not py_versions:
|
|
263
271
|
msg = f"No known Python versions match {self.python!r}"
|
|
264
272
|
raise ValueError(msg)
|
|
265
273
|
if self.python_order == "desc":
|
|
266
274
|
py_versions.reverse()
|
|
267
|
-
patches = self.python_patches or {}
|
|
268
275
|
multi_impl = len(self.implementations) > 1
|
|
269
276
|
return [
|
|
270
277
|
MatrixTuple(
|
|
@@ -201,9 +201,12 @@ def _resolve_one_tuple_with_overrides( # noqa: PLR0913
|
|
|
201
201
|
resolution_strategy: str,
|
|
202
202
|
) -> dict[str, str]:
|
|
203
203
|
"""Re-resolve ``tup`` with the given metadata overrides; return pins."""
|
|
204
|
+
# Carry the original tuple's patch release so markers gated on
|
|
205
|
+
# python_full_version evaluate the same in both passes.
|
|
204
206
|
one_tuple_matrix = _Matrix(
|
|
205
207
|
python=f"=={tup.python_version}",
|
|
206
208
|
platforms=(tup.platform_spec,),
|
|
209
|
+
python_patches={tup.python_version: tup.environment["python_full_version"]},
|
|
207
210
|
implementations=(tup.implementation,),
|
|
208
211
|
)
|
|
209
212
|
with _override_metadata(coordinator, wheel_metadata):
|
|
@@ -294,17 +297,24 @@ def _override_metadata(
|
|
|
294
297
|
overridden raw text rather than serving the prior tuple's view.
|
|
295
298
|
"""
|
|
296
299
|
text_snapshot: dict[tuple[str, str], str | None] = {}
|
|
300
|
+
from_sdist_snapshot: dict[tuple[str, str], bool] = {}
|
|
297
301
|
parsed_snapshot: dict[tuple[str, str], object | None] = {}
|
|
298
302
|
for key in overrides:
|
|
299
303
|
text_snapshot[key] = coordinator.index.get_metadata(*key)
|
|
304
|
+
from_sdist_snapshot[key] = coordinator.index.metadata_from_sdist(*key)
|
|
300
305
|
parsed_snapshot[key] = coordinator.index.pop_parsed_metadata(*key)
|
|
301
306
|
try:
|
|
302
307
|
for key, text in overrides.items():
|
|
303
308
|
coordinator.index.store_metadata(*key, text)
|
|
304
309
|
yield
|
|
305
310
|
finally:
|
|
311
|
+
# Restore through the matching store call so the slot's sdist
|
|
312
|
+
# provenance survives the override cycle.
|
|
306
313
|
for key, prior in text_snapshot.items():
|
|
307
|
-
|
|
314
|
+
if from_sdist_snapshot[key]:
|
|
315
|
+
coordinator.index.store_sdist_metadata(*key, prior)
|
|
316
|
+
else:
|
|
317
|
+
coordinator.index.store_metadata(*key, prior)
|
|
308
318
|
for key, parsed in parsed_snapshot.items():
|
|
309
319
|
coordinator.index.pop_parsed_metadata(*key)
|
|
310
320
|
if parsed is not None:
|
nab_python/universal/validate.py
CHANGED
|
@@ -26,7 +26,7 @@ from .._vendor.packaging.requirements import (
|
|
|
26
26
|
InvalidRequirement,
|
|
27
27
|
Requirement,
|
|
28
28
|
)
|
|
29
|
-
from .._vendor.packaging.utils import canonicalize_name
|
|
29
|
+
from .._vendor.packaging.utils import canonicalize_name, canonicalize_version
|
|
30
30
|
from ..metadata import load_static_project, metadata_deps_are_static, parse_metadata
|
|
31
31
|
from .wheel_selection import select_wheel_for_tuple
|
|
32
32
|
|
|
@@ -47,7 +47,9 @@ __all__ = [
|
|
|
47
47
|
|
|
48
48
|
# Statuses that always fail the lock at install time, regardless of
|
|
49
49
|
# build policy.
|
|
50
|
-
_ALWAYS_FATAL_STATUSES = frozenset(
|
|
50
|
+
_ALWAYS_FATAL_STATUSES = frozenset(
|
|
51
|
+
{"version_not_found", "no_compatible_wheel", "no_metadata"}
|
|
52
|
+
)
|
|
51
53
|
|
|
52
54
|
# Statuses that fail only when the build policy refuses to build
|
|
53
55
|
# from sdist (BuildPolicy.NEVER). These pins resolve fine if the
|
|
@@ -77,8 +79,10 @@ class PinValidation:
|
|
|
77
79
|
|
|
78
80
|
- ``ok``: chosen wheel's deps (after marker eval) match the resolver's.
|
|
79
81
|
- ``divergent``: wheel has metadata but deps differ from baseline.
|
|
80
|
-
- ``sdist_only``: no wheels
|
|
81
|
-
Fatal under ``BuildPolicy.NEVER``.
|
|
82
|
+
- ``sdist_only``: an sdist but no wheels; the user must build from
|
|
83
|
+
sdist. Fatal under ``BuildPolicy.NEVER``.
|
|
84
|
+
- ``version_not_found``: the index has no files at the pinned
|
|
85
|
+
version (or no listing for the package). Always fatal.
|
|
82
86
|
- ``no_compatible_wheel``: wheels exist but none match the tuple's
|
|
83
87
|
tags and no buildable sdist exists. Always fatal.
|
|
84
88
|
- ``no_compatible_wheel_with_sdist``: as above but a sdist is
|
|
@@ -112,9 +116,10 @@ class ValidationReport:
|
|
|
112
116
|
def fatal_findings(self, *, build_allowed: bool = False) -> list[PinValidation]:
|
|
113
117
|
"""Return findings that would prevent installation.
|
|
114
118
|
|
|
115
|
-
Always fatal: ``
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
Always fatal: ``version_not_found`` (nothing at the pinned
|
|
120
|
+
version), ``no_compatible_wheel`` (no installable artifact)
|
|
121
|
+
and ``no_metadata`` (we can't trust the resolver ran with
|
|
122
|
+
the right deps).
|
|
118
123
|
|
|
119
124
|
Fatal under BuildPolicy.NEVER: ``sdist_only`` and
|
|
120
125
|
``no_compatible_wheel_with_sdist`` (only sdist available).
|
|
@@ -165,6 +170,14 @@ def _validate_pin( # noqa: PLR0911 - one return per outcome reads cleaner here
|
|
|
165
170
|
files_at_version = [f for f in listing if f.version == str(version)]
|
|
166
171
|
wheels_at_version = [f for f in files_at_version if isinstance(f, WheelFile)]
|
|
167
172
|
has_sdist = any(not isinstance(f, WheelFile) for f in files_at_version)
|
|
173
|
+
if not files_at_version:
|
|
174
|
+
return PinValidation(
|
|
175
|
+
tuple_label=tup.label,
|
|
176
|
+
package=package,
|
|
177
|
+
version=str(version),
|
|
178
|
+
status="version_not_found",
|
|
179
|
+
detail="index has no files at this version; nothing to install or build",
|
|
180
|
+
)
|
|
168
181
|
if not wheels_at_version:
|
|
169
182
|
return PinValidation(
|
|
170
183
|
tuple_label=tup.label,
|
|
@@ -408,7 +421,9 @@ def _evaluate_metadata_deps_by_extra(
|
|
|
408
421
|
references go to the base bucket; markers with ``extra ==
|
|
409
422
|
"name"`` go to that named bucket. This lets the validator catch
|
|
410
423
|
per-extra divergence between the resolver's listing baseline and
|
|
411
|
-
the chosen wheel, even when base deps match.
|
|
424
|
+
the chosen wheel, even when base deps match. Bucket members are
|
|
425
|
+
:func:`_requirement_key` strings, so two wheels that agree on dep
|
|
426
|
+
names but disagree on specifiers still diverge.
|
|
412
427
|
"""
|
|
413
428
|
metadata = parse_metadata(metadata_text)
|
|
414
429
|
extras = {canonicalize_name(e) for e in metadata.provides_extra}
|
|
@@ -421,12 +436,35 @@ def _evaluate_metadata_deps_by_extra(
|
|
|
421
436
|
req = Requirement(str(req_text))
|
|
422
437
|
except InvalidRequirement: # pragma: no cover
|
|
423
438
|
continue
|
|
424
|
-
|
|
439
|
+
key = _requirement_key(req)
|
|
425
440
|
marker = req.marker
|
|
426
441
|
if marker is None or marker.evaluate(base_env):
|
|
427
|
-
out[None].add(
|
|
442
|
+
out[None].add(key)
|
|
428
443
|
continue
|
|
429
444
|
for e in extras:
|
|
430
445
|
if marker.evaluate({**environment, "extra": e}):
|
|
431
|
-
out[e].add(
|
|
446
|
+
out[e].add(key)
|
|
432
447
|
return out
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _requirement_key(req: Requirement) -> str:
|
|
451
|
+
"""Render a requirement's name, extras and constraint canonically.
|
|
452
|
+
|
|
453
|
+
Versions are canonicalized per specifier so equivalent spellings
|
|
454
|
+
(``>=2.0`` vs ``>=2``) compare equal across wheels; the marker is
|
|
455
|
+
omitted because bucketing by extra already accounts for it.
|
|
456
|
+
"""
|
|
457
|
+
name: str = canonicalize_name(req.name)
|
|
458
|
+
if req.extras:
|
|
459
|
+
name += "[" + ",".join(sorted(canonicalize_name(e) for e in req.extras)) + "]"
|
|
460
|
+
if req.url is not None:
|
|
461
|
+
return f"{name} @ {req.url}"
|
|
462
|
+
return name + ",".join(
|
|
463
|
+
sorted(
|
|
464
|
+
spec.operator
|
|
465
|
+
+ canonicalize_version(
|
|
466
|
+
spec.version, strip_trailing_zero=spec.operator != "~="
|
|
467
|
+
)
|
|
468
|
+
for spec in req.specifier
|
|
469
|
+
)
|
|
470
|
+
)
|
|
@@ -108,8 +108,8 @@ class PlatformSpec:
|
|
|
108
108
|
("glibc", _floor_tag(self.manylinux_floor)),
|
|
109
109
|
("musl", _floor_tag(self.musllinux_floor)),
|
|
110
110
|
("macos", _floor_tag(self.macos_min)),
|
|
111
|
-
("rel",
|
|
112
|
-
("ver",
|
|
111
|
+
("rel", _escape_label_value(self.platform_release)),
|
|
112
|
+
("ver", _escape_label_value(self.platform_version)),
|
|
113
113
|
)
|
|
114
114
|
return "".join(f"-{tag}{value}" for tag, value in fields if value)
|
|
115
115
|
|
|
@@ -138,6 +138,26 @@ def _floor_tag(floor: tuple[int, int] | None) -> str:
|
|
|
138
138
|
return f"{floor[0]}.{floor[1]}" if floor is not None else ""
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
def _escape_label_value(value: str) -> str:
|
|
142
|
+
"""Escape a free-form marker value for a label suffix field.
|
|
143
|
+
|
|
144
|
+
Alphanumerics and ``.`` pass through, ``_`` doubles itself, and any
|
|
145
|
+
other character becomes ``_<hex codepoint>_``. This keeps the
|
|
146
|
+
encoding injective and the output free of ``-``, so a value can
|
|
147
|
+
never forge a field boundary and collapse two distinct specs onto
|
|
148
|
+
one label.
|
|
149
|
+
"""
|
|
150
|
+
out: list[str] = []
|
|
151
|
+
for ch in value:
|
|
152
|
+
if ch == "_":
|
|
153
|
+
out.append("__")
|
|
154
|
+
elif ch.isalnum() or ch == ".":
|
|
155
|
+
out.append(ch)
|
|
156
|
+
else:
|
|
157
|
+
out.append(f"_{ord(ch):x}_")
|
|
158
|
+
return "".join(out)
|
|
159
|
+
|
|
160
|
+
|
|
141
161
|
def _linux_platform_tags(
|
|
142
162
|
arch: str,
|
|
143
163
|
*,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nab-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Index-backed provider, lockfile emitter, and downloader for nab
|
|
5
5
|
Project-URL: Homepage, https://github.com/notatallshaw/nab
|
|
6
6
|
Project-URL: Documentation, https://nab.readthedocs.io/
|
|
@@ -19,8 +19,8 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: build>=1.2
|
|
21
21
|
Requires-Dist: installer>=0.7
|
|
22
|
-
Requires-Dist: nab-index==0.0.
|
|
23
|
-
Requires-Dist: nab-resolver==0.0.
|
|
22
|
+
Requires-Dist: nab-index==0.0.5
|
|
23
|
+
Requires-Dist: nab-resolver==0.0.5
|
|
24
24
|
Requires-Dist: pyproject-hooks>=1.2
|
|
25
25
|
Requires-Dist: tomli-w>=1.2
|
|
26
26
|
Requires-Dist: tomli>=2.0
|
|
@@ -2,17 +2,17 @@ nab_python/__init__.py,sha256=tKRFGIR13xvNEftDBxxdczeKtcrvaqcOPiYbmdyFOOs,62
|
|
|
2
2
|
nab_python/_conflict_kind.py,sha256=ZDVR_8WvyNnxvk0v5pqupKWN3CoSi4DpV6mr4yEOxRE,755
|
|
3
3
|
nab_python/_packaging_provider.py,sha256=-dxbaed8UoPtqxBnrOqxw2N_jPzHaQEMT1RJOoMHdRI,3435
|
|
4
4
|
nab_python/_toml.py,sha256=r_8CfSxoNHWsnchzmdHEsyzd0iigBp9XWDszUN5mhSM,581
|
|
5
|
-
nab_python/_vcs_admission.py,sha256=
|
|
5
|
+
nab_python/_vcs_admission.py,sha256=wG9S0naO7nfVI-jJrEyweJUsk9Avv4KBLUA-ga0xjZw,6406
|
|
6
6
|
nab_python/build_backend.py,sha256=T-KBp-VulIQATUjYvTt_hJronFceDSjvB-wZ3YHWPpw,6519
|
|
7
7
|
nab_python/config.py,sha256=LplXzroZluUFKtN7WuaBKVEquerrAcfrRA8dkfY7H7M,53288
|
|
8
|
-
nab_python/download.py,sha256=
|
|
9
|
-
nab_python/fetch.py,sha256=
|
|
10
|
-
nab_python/lockfile.py,sha256=
|
|
11
|
-
nab_python/metadata.py,sha256=
|
|
12
|
-
nab_python/provider.py,sha256=
|
|
8
|
+
nab_python/download.py,sha256=tPf6bryLbM85hez88nvqmBJr6GdGj1Ux8tNdRmfeqS8,6850
|
|
9
|
+
nab_python/fetch.py,sha256=aZZu-H-3uScF4kNLh45Cy5Ur1TNUF0GxauwX7NWsC3s,35180
|
|
10
|
+
nab_python/lockfile.py,sha256=bAO9WybifQd2v7QSOcTmKcBoxKB7jVhuiAaE0nbWUbE,11428
|
|
11
|
+
nab_python/metadata.py,sha256=_1CvzZ7O8jricjBhPm3AfS3-yrdgL8F_2xpVqBq_ZqY,6586
|
|
12
|
+
nab_python/provider.py,sha256=teuTYZhxAm16qGNsicaLckFDku23VaKAz3oOeoSKZco,53512
|
|
13
13
|
nab_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
nab_python/requirements_file.py,sha256=YdXc9wdomhEWwxwUwKPa6kCcQ5c5E0IpXqufo8oOV0Y,10708
|
|
15
|
-
nab_python/resolve.py,sha256=
|
|
15
|
+
nab_python/resolve.py,sha256=5V-9TctP_PowlNYZglRkHqU3STJPY5aM3JtXjwsRwGE,34654
|
|
16
16
|
nab_python/workspace.py,sha256=WhTUIgFP0IKigmeUvaV1S0j-lK9IHEjhMy57OK0rGpY,8270
|
|
17
17
|
nab_python/_build/__init__.py,sha256=7qW6Gk8Tr8fc3xon-yZ1zW5Z1oUU-LPRpgyucso1GNc,61
|
|
18
18
|
nab_python/_build/env.py,sha256=hhst05zByIn0ZblYlsitJTdCWJZMEm1MzUG24xWMaSw,14036
|
|
@@ -21,15 +21,15 @@ nab_python/_build/runner.py,sha256=6yV1sz4bxOuOAgPp5vpO1w8X1EifDLcB9vSCOzxsLYM,9
|
|
|
21
21
|
nab_python/_lockfile/__init__.py,sha256=gfbgTLr4C66nEZeaI5G5DGGo6_s_qrB2uWAo3DjXC7Q,59
|
|
22
22
|
nab_python/_lockfile/builder.py,sha256=YPRb35YTWhd68B7-nXTfrbtJnPE3tTGcdWGakqkkWv0,20528
|
|
23
23
|
nab_python/_lockfile/disjointness.py,sha256=CyEuinsVkIoPjTjE_CFzRi5mvp8X7CVSGQZbNCVup3g,17212
|
|
24
|
-
nab_python/_lockfile/pylock.py,sha256=
|
|
25
|
-
nab_python/_lockfile/requirements.py,sha256=
|
|
24
|
+
nab_python/_lockfile/pylock.py,sha256=m2CRlPaIBr_qufVXFFDYx6YPkekQ8-EDF6tQujzGPTM,24678
|
|
25
|
+
nab_python/_lockfile/requirements.py,sha256=5FRO0ul9stXumRUHt2q5fIO0q1ogW-LYt26ajA6t4hs,4797
|
|
26
26
|
nab_python/_provider/__init__.py,sha256=FJwoSdUfrfhhXfaeJAIXQEWzY6KV4hKFNnvRkQVccfA,59
|
|
27
27
|
nab_python/_provider/build_remote.py,sha256=nqQ4rzXNkWxNouAyhhDC-rPZmtSsbPOaREoAPYm4Oz4,3411
|
|
28
|
-
nab_python/_provider/extras.py,sha256=
|
|
29
|
-
nab_python/_provider/listing.py,sha256=
|
|
28
|
+
nab_python/_provider/extras.py,sha256=Jom9EMtEMJc5WI3yhqbbkuYTrn8XTt1iRMdnaHe9iys,8574
|
|
29
|
+
nab_python/_provider/listing.py,sha256=QnM4uGqdsEBTLfLD-STRsSBV6iW40SKNixUuJpfiXJk,17791
|
|
30
30
|
nab_python/_provider/lookahead.py,sha256=xgX0f4h9dt2rdXZ_HmQmzXKyac2H4pDoaRMpVUqFotQ,5895
|
|
31
|
-
nab_python/_provider/metadata_resolver.py,sha256=
|
|
32
|
-
nab_python/_provider/priority.py,sha256=
|
|
31
|
+
nab_python/_provider/metadata_resolver.py,sha256=CVIAUadBSprWAj2Ti_UmsJ2Id7AfkVp_Wc3HFZ-nYsQ,17252
|
|
32
|
+
nab_python/_provider/priority.py,sha256=vdtS08Gf9kAtoXQ2xa5-WCA7r-dCYpOwfzMT4Curfks,6098
|
|
33
33
|
nab_python/_provider/sources.py,sha256=DeG3oQv3iCrZPgW9VsM_6c5EDiVF3b5jWvdvx7TH9M8,7837
|
|
34
34
|
nab_python/_testing/__init__.py,sha256=WiMwVVwq6kg2mHFeM7JNA4-2tiXWVOPXq6YJd49vJQ0,44
|
|
35
35
|
nab_python/_testing/coordinator_fake.py,sha256=4RDaZkEHwEHt1hK-dgUqMZbcrJhbvVSbDihMD710_gE,9311
|
|
@@ -63,12 +63,12 @@ nab_python/_vendor/packaging/version.py,sha256=daDMk4YRlKNgSiQkkIlxfmpsj_qGiWhVg
|
|
|
63
63
|
nab_python/_vendor/packaging/licenses/__init__.py,sha256=_Jx0XRiD_58palsWnyLrLuh59ZpGCPIPXLKdZo9OJvQ,7293
|
|
64
64
|
nab_python/_vendor/packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
|
|
65
65
|
nab_python/universal/__init__.py,sha256=eNFMbsK-jBNy1J7wAE702vI-LVXF_aaQQvX7G02ImUw,48
|
|
66
|
-
nab_python/universal/matrix.py,sha256=
|
|
66
|
+
nab_python/universal/matrix.py,sha256=DSXzmojLGCJQI_Qgy6m2W8P-AziFuCcDzgNcYwsBIk0,13059
|
|
67
67
|
nab_python/universal/provider.py,sha256=lAHEJ8HQPK1nWvNhKaKoFFRQsxTkliW-QY07W42WK3M,9904
|
|
68
|
-
nab_python/universal/reresolve.py,sha256=
|
|
68
|
+
nab_python/universal/reresolve.py,sha256=iA7_QRnLJbdAaN0XS1woLRDVRKBmggIUqLfZZUqZbO0,12195
|
|
69
69
|
nab_python/universal/resolve.py,sha256=M1r_Tb1kvG3SMlXGKAUWhTGqc3A0Ah1GomMa4CSlGKU,28351
|
|
70
|
-
nab_python/universal/validate.py,sha256
|
|
71
|
-
nab_python/universal/wheel_selection.py,sha256=
|
|
72
|
-
nab_python-0.0.
|
|
73
|
-
nab_python-0.0.
|
|
74
|
-
nab_python-0.0.
|
|
70
|
+
nab_python/universal/validate.py,sha256=-T084FTML0joZcfUOyiooLk56fL_0pfXi7lVb7bjLjs,17408
|
|
71
|
+
nab_python/universal/wheel_selection.py,sha256=A7Ozb6jOCDkw5qp1GZ55TmBTrUTpIN7qmI19LXW02JI,14425
|
|
72
|
+
nab_python-0.0.5.dist-info/METADATA,sha256=mkirolKaVgtidr-7EHQNuxp6VDPrfPMYsky0eQjMXvY,1768
|
|
73
|
+
nab_python-0.0.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
74
|
+
nab_python-0.0.5.dist-info/RECORD,,
|
|
File without changes
|