nab-python 0.0.3__py3-none-any.whl → 0.0.4__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/runner.py +14 -3
- nab_python/_conflict_kind.py +20 -0
- nab_python/_lockfile/builder.py +158 -30
- nab_python/_lockfile/disjointness.py +251 -51
- nab_python/_lockfile/pylock.py +205 -29
- nab_python/_lockfile/requirements.py +8 -7
- nab_python/_provider/extras.py +7 -7
- nab_python/_provider/listing.py +29 -9
- nab_python/_provider/metadata_resolver.py +24 -10
- nab_python/_testing/coordinator_fake.py +12 -4
- nab_python/_toml.py +16 -0
- nab_python/_vendor/packaging/PROVENANCE.md +2 -2
- nab_python/_vendor/packaging/_range_utils.py +337 -112
- nab_python/_vendor/packaging/_version_utils.py +66 -13
- nab_python/_vendor/packaging/metadata.py +19 -4
- nab_python/_vendor/packaging/pylock.py +1 -0
- nab_python/_vendor/packaging/ranges.py +1412 -443
- nab_python/_vendor/packaging/specifiers.py +291 -159
- nab_python/_vendor/packaging/tags.py +8 -6
- nab_python/config.py +609 -11
- nab_python/download.py +39 -9
- nab_python/fetch.py +60 -15
- nab_python/lockfile.py +56 -0
- nab_python/metadata.py +23 -0
- nab_python/provider.py +77 -33
- nab_python/requirements_file.py +100 -15
- nab_python/resolve.py +444 -30
- nab_python/universal/matrix.py +130 -33
- nab_python/universal/provider.py +20 -5
- nab_python/universal/reresolve.py +1 -0
- nab_python/universal/resolve.py +230 -32
- nab_python/universal/validate.py +8 -15
- nab_python/universal/wheel_selection.py +92 -57
- nab_python/workspace.py +8 -2
- {nab_python-0.0.3.dist-info → nab_python-0.0.4.dist-info}/METADATA +3 -3
- {nab_python-0.0.3.dist-info → nab_python-0.0.4.dist-info}/RECORD +37 -35
- {nab_python-0.0.3.dist-info → nab_python-0.0.4.dist-info}/WHEEL +0 -0
nab_python/_build/runner.py
CHANGED
|
@@ -34,11 +34,13 @@ import pyproject_hooks
|
|
|
34
34
|
import tomli
|
|
35
35
|
|
|
36
36
|
from .._vendor.packaging.requirements import Requirement
|
|
37
|
-
from .._vendor.packaging.specifiers import SpecifierSet
|
|
37
|
+
from .._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
38
38
|
from .._vendor.packaging.utils import canonicalize_name
|
|
39
39
|
from .._vendor.packaging.version import InvalidVersion, Version
|
|
40
|
+
from nab_resolver.resolver import ResolutionError
|
|
41
|
+
|
|
40
42
|
from ..metadata import WheelMetadata
|
|
41
|
-
from .env import NabBuildEnv
|
|
43
|
+
from .env import BuildEnvError, NabBuildEnv
|
|
42
44
|
from .errors import BuildBackendError
|
|
43
45
|
|
|
44
46
|
if TYPE_CHECKING:
|
|
@@ -121,6 +123,9 @@ def run_build_backend(
|
|
|
121
123
|
except build.BuildBackendException as exc:
|
|
122
124
|
msg = f"build backend {backend!r} failed: {exc}"
|
|
123
125
|
raise BuildBackendError(msg) from exc
|
|
126
|
+
except (BuildEnvError, ResolutionError) as exc:
|
|
127
|
+
msg = f"build env setup for {backend!r} failed: {exc}"
|
|
128
|
+
raise BuildBackendError(msg) from exc
|
|
124
129
|
|
|
125
130
|
|
|
126
131
|
def _read_build_system(
|
|
@@ -228,7 +233,13 @@ def _parse_metadata(metadata_path: Path) -> WheelMetadata:
|
|
|
228
233
|
raise BuildBackendError(msg) from exc
|
|
229
234
|
|
|
230
235
|
requires_python_raw = msg_obj.get("Requires-Python")
|
|
231
|
-
|
|
236
|
+
try:
|
|
237
|
+
requires_python = (
|
|
238
|
+
SpecifierSet(requires_python_raw) if requires_python_raw else None
|
|
239
|
+
)
|
|
240
|
+
except InvalidSpecifier as exc:
|
|
241
|
+
msg = f"backend METADATA has invalid Requires-Python {requires_python_raw!r}: {exc}"
|
|
242
|
+
raise BuildBackendError(msg) from exc
|
|
232
243
|
|
|
233
244
|
requires_dist: list[Requirement] = []
|
|
234
245
|
for raw in msg_obj.get_all("Requires-Dist") or ():
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Conflict-kind constants and PEP 508 marker-variable mapping.
|
|
2
|
+
|
|
3
|
+
A leaf module that :mod:`nab_python.config`, :mod:`nab_python.universal.matrix`,
|
|
4
|
+
and :mod:`nab_python._lockfile.disjointness` can import without forming a
|
|
5
|
+
cycle. :class:`nab_python.config.ConflictKind` takes its enum values from
|
|
6
|
+
``KIND_EXTRA`` / ``KIND_GROUP`` so a rename here flows to every consumer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
KIND_EXTRA = "extra"
|
|
12
|
+
KIND_GROUP = "group"
|
|
13
|
+
|
|
14
|
+
# Membership of a conflict-fork member emits ``'name' in <variable>`` on
|
|
15
|
+
# the per-package marker; this mapping is the (kind -> variable) contract
|
|
16
|
+
# the universal matrix and the disjointness validator share.
|
|
17
|
+
MARKER_VARIABLE_FOR_KIND = {
|
|
18
|
+
KIND_EXTRA: "extras",
|
|
19
|
+
KIND_GROUP: "dependency_groups",
|
|
20
|
+
}
|
nab_python/_lockfile/builder.py
CHANGED
|
@@ -9,6 +9,7 @@ be read directly without a second fetch. This module also owns the
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from collections import defaultdict
|
|
12
13
|
from datetime import datetime, timezone
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import TYPE_CHECKING, Protocol, overload
|
|
@@ -18,6 +19,7 @@ import tomli
|
|
|
18
19
|
|
|
19
20
|
from nab_index.client import SdistFile, WheelFile
|
|
20
21
|
|
|
22
|
+
from .._toml import tool_nab_section
|
|
21
23
|
from .._vendor.packaging.pylock import Pylock, PylockValidationError
|
|
22
24
|
from .._vendor.packaging.utils import canonicalize_name
|
|
23
25
|
|
|
@@ -40,6 +42,8 @@ if TYPE_CHECKING:
|
|
|
40
42
|
|
|
41
43
|
__all__ = [
|
|
42
44
|
"MissingHashError",
|
|
45
|
+
"MissingSdistError",
|
|
46
|
+
"MissingVcsCommitError",
|
|
43
47
|
"build_lock_input_from_provider",
|
|
44
48
|
"read_lockfile_anchor",
|
|
45
49
|
"read_lockfile_packages",
|
|
@@ -71,6 +75,12 @@ class LockInputProvider(Protocol):
|
|
|
71
75
|
may supply a stub without inheriting the full Provider class.
|
|
72
76
|
"""
|
|
73
77
|
|
|
78
|
+
deps_cache: Mapping[tuple[str, Version], Mapping[str, object]]
|
|
79
|
+
"""Direct dependencies per ``(canonical name, version)``."""
|
|
80
|
+
|
|
81
|
+
extra_deps_map: Mapping[tuple[str, Version], Mapping[str, Mapping[str, object]]]
|
|
82
|
+
"""Per-extra dependencies per ``(canonical name, version)``."""
|
|
83
|
+
|
|
74
84
|
@property
|
|
75
85
|
def coordinator(self) -> _LockInputCoordinator:
|
|
76
86
|
"""Coordinator used to look up the index that served a listing."""
|
|
@@ -111,6 +121,30 @@ class MissingHashError(ValueError):
|
|
|
111
121
|
"""
|
|
112
122
|
|
|
113
123
|
|
|
124
|
+
class MissingSdistError(ValueError):
|
|
125
|
+
"""A ``sdist-install`` package's pinned version has no sdist.
|
|
126
|
+
|
|
127
|
+
Under :attr:`~nab_python.provider.DistPolicy.SDIST_INSTALL` the
|
|
128
|
+
resolver may read a wheel's metadata but the lock must pin only the
|
|
129
|
+
sdist. When the pinned version publishes wheels but no sdist, the
|
|
130
|
+
wheels are dropped and nothing is left to pin. Surface the package
|
|
131
|
+
and version so the user can pick a version with an sdist or relax
|
|
132
|
+
the policy, rather than emitting an empty package the spec rejects.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class MissingVcsCommitError(ValueError):
|
|
137
|
+
"""A VCS source reached the lock writer without a resolved commit SHA.
|
|
138
|
+
|
|
139
|
+
PEP 751 requires ``packages.vcs.commit-id`` to be an immutable
|
|
140
|
+
identifier. nab records the post-clone SHA on the provider during
|
|
141
|
+
materialisation, before any version can be pinned, so a missing SHA
|
|
142
|
+
here means a VCS source was pinned without being cloned. Surface it
|
|
143
|
+
loudly rather than emit a branch name or empty string as the commit
|
|
144
|
+
id, which would silently produce a non-reproducible lock.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
|
|
114
148
|
def read_lockfile_anchor(path: Path) -> datetime | None:
|
|
115
149
|
"""Return the ``[tool.nab].created-at`` timestamp from ``path`` if any.
|
|
116
150
|
|
|
@@ -131,7 +165,8 @@ def read_lockfile_anchor(path: Path) -> datetime | None:
|
|
|
131
165
|
data = tomli.load(f)
|
|
132
166
|
except (OSError, tomli.TOMLDecodeError):
|
|
133
167
|
return None
|
|
134
|
-
|
|
168
|
+
nab = tool_nab_section(data)
|
|
169
|
+
raw = nab.get("created-at") if isinstance(nab, dict) else None
|
|
135
170
|
if isinstance(raw, datetime):
|
|
136
171
|
return raw if raw.tzinfo else raw.replace(tzinfo=timezone.utc)
|
|
137
172
|
if isinstance(raw, str):
|
|
@@ -183,7 +218,7 @@ def _strip_userinfo(url: str) -> str:
|
|
|
183
218
|
return urlunsplit(parts._replace(netloc=netloc))
|
|
184
219
|
|
|
185
220
|
|
|
186
|
-
def build_lock_input_from_provider(
|
|
221
|
+
def build_lock_input_from_provider( # noqa: PLR0913 - each flag maps to a distinct lockfile field
|
|
187
222
|
provider: LockInputProvider,
|
|
188
223
|
pins: Mapping[str, Version],
|
|
189
224
|
*,
|
|
@@ -193,6 +228,7 @@ def build_lock_input_from_provider(
|
|
|
193
228
|
default_groups: Sequence[str] = (),
|
|
194
229
|
created_by: str = "nab",
|
|
195
230
|
indexes: Sequence[IndexConfig] = (),
|
|
231
|
+
resolved_keys: Iterable[str] = (),
|
|
196
232
|
) -> LockInput:
|
|
197
233
|
"""Build a :class:`LockInput` from a finished resolve.
|
|
198
234
|
|
|
@@ -205,6 +241,10 @@ def build_lock_input_from_provider(
|
|
|
205
241
|
were folded into this resolve; ``default_groups`` is the subset
|
|
206
242
|
that a default install (no ``--group`` flag) should apply.
|
|
207
243
|
|
|
244
|
+
``resolved_keys`` is the full set of resolver result keys, including
|
|
245
|
+
``name[extra]`` proxies; it is read to find which extras activated
|
|
246
|
+
so their edges join the forward dependency graph.
|
|
247
|
+
|
|
208
248
|
All wheels and the sdist for each pinned version are recorded so
|
|
209
249
|
the lockfile is portable across architectures of the same Python.
|
|
210
250
|
"""
|
|
@@ -242,9 +282,53 @@ def build_lock_input_from_provider(
|
|
|
242
282
|
extras=tuple(extras),
|
|
243
283
|
dependency_groups=tuple(dependency_groups),
|
|
244
284
|
default_groups=tuple(default_groups),
|
|
285
|
+
dependencies=_forward_dependency_graph(provider, pins, resolved_keys),
|
|
245
286
|
)
|
|
246
287
|
|
|
247
288
|
|
|
289
|
+
def _forward_dependency_graph(
|
|
290
|
+
provider: LockInputProvider,
|
|
291
|
+
pins: Mapping[str, Version],
|
|
292
|
+
resolved_keys: Iterable[str],
|
|
293
|
+
) -> dict[str, tuple[str, ...]]:
|
|
294
|
+
"""Build the forward dependency graph among the locked packages.
|
|
295
|
+
|
|
296
|
+
Each pinned package maps to the canonical names of its direct
|
|
297
|
+
dependencies that are themselves pinned. Base dependencies come
|
|
298
|
+
from ``deps_cache``; an activated extra (a ``name[extra]`` key in
|
|
299
|
+
``resolved_keys``) folds that extra's dependencies in too. Names
|
|
300
|
+
not in ``pins`` are dropped so every edge points at a real
|
|
301
|
+
``[[packages]]`` entry.
|
|
302
|
+
"""
|
|
303
|
+
from ..provider import split_extra
|
|
304
|
+
|
|
305
|
+
activated_extras: defaultdict[str, set[str]] = defaultdict(set)
|
|
306
|
+
for key in resolved_keys:
|
|
307
|
+
base, extra = split_extra(key)
|
|
308
|
+
if extra is not None:
|
|
309
|
+
activated_extras[canonicalize_name(base)].add(extra)
|
|
310
|
+
|
|
311
|
+
pinned = {canonicalize_name(name) for name in pins}
|
|
312
|
+
graph: dict[str, tuple[str, ...]] = {}
|
|
313
|
+
for raw_name, version in pins.items():
|
|
314
|
+
canonical = canonicalize_name(raw_name)
|
|
315
|
+
cache_key = (canonical, version)
|
|
316
|
+
dep_names = {
|
|
317
|
+
canonicalize_name(split_extra(dep)[0])
|
|
318
|
+
for dep in provider.deps_cache.get(cache_key, {})
|
|
319
|
+
}
|
|
320
|
+
extra_map = provider.extra_deps_map.get(cache_key, {})
|
|
321
|
+
for extra in activated_extras.get(canonical, ()):
|
|
322
|
+
dep_names.update(
|
|
323
|
+
canonicalize_name(split_extra(dep)[0])
|
|
324
|
+
for dep in extra_map.get(extra, {})
|
|
325
|
+
)
|
|
326
|
+
dep_names &= pinned
|
|
327
|
+
if dep_names:
|
|
328
|
+
graph[canonical] = tuple(sorted(dep_names))
|
|
329
|
+
return graph
|
|
330
|
+
|
|
331
|
+
|
|
248
332
|
def _index_pin_from_listing(
|
|
249
333
|
provider: LockInputProvider,
|
|
250
334
|
canonical: str,
|
|
@@ -257,8 +341,8 @@ def _index_pin_from_listing(
|
|
|
257
341
|
served the package's listing during the resolve, looked up from
|
|
258
342
|
the coordinator's :class:`InMemoryIndex` (which records the
|
|
259
343
|
serving index by name) and resolved against ``indexes`` for the
|
|
260
|
-
URL.
|
|
261
|
-
|
|
344
|
+
URL. A pinned package's serving index is always recorded and is
|
|
345
|
+
one of ``indexes``, so the URL is always known.
|
|
262
346
|
|
|
263
347
|
Under :attr:`~nab_python.provider.DistPolicy.SDIST_INSTALL` the
|
|
264
348
|
package's wheels stayed in ``versions_cache`` as a possible
|
|
@@ -272,6 +356,13 @@ def _index_pin_from_listing(
|
|
|
272
356
|
files = list(provider.dist_files_for(canonical, version))
|
|
273
357
|
if provider.effective_dist_policy(canonical) is DistPolicy.SDIST_INSTALL:
|
|
274
358
|
files = [f for f in files if not isinstance(f, WheelFile)]
|
|
359
|
+
if not any(isinstance(f, SdistFile) for f in files):
|
|
360
|
+
msg = (
|
|
361
|
+
f"{canonical}=={version} has no sdist, but its dist-policy is "
|
|
362
|
+
f"'sdist-install'; pick a version that publishes an sdist or "
|
|
363
|
+
f"change dist-policy for {canonical}"
|
|
364
|
+
)
|
|
365
|
+
raise MissingSdistError(msg)
|
|
275
366
|
|
|
276
367
|
wheels = tuple(
|
|
277
368
|
_build_artifact(canonical, f, WheelArtifact)
|
|
@@ -287,12 +378,17 @@ def _index_pin_from_listing(
|
|
|
287
378
|
requires_python = _common_requires_python(files)
|
|
288
379
|
serving_name = provider.coordinator.index.get_listing_index(canonical)
|
|
289
380
|
by_name = {ix.name: ix.url for ix in indexes}
|
|
290
|
-
|
|
291
|
-
if
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
381
|
+
index_url = by_name.get(serving_name) if serving_name is not None else None
|
|
382
|
+
if index_url is None:
|
|
383
|
+
if by_name:
|
|
384
|
+
# Can't happen for a real pin (the serving index is always
|
|
385
|
+
# recorded and configured); raise, don't guess.
|
|
386
|
+
msg = (
|
|
387
|
+
f"{canonical}: recorded serving index {serving_name!r} is "
|
|
388
|
+
"not one of the configured indexes"
|
|
389
|
+
)
|
|
390
|
+
raise AssertionError(msg)
|
|
391
|
+
# No indexes configured (unit tests only); use the default root.
|
|
296
392
|
index_url = DEFAULT_INDEX_URL
|
|
297
393
|
return IndexPin(
|
|
298
394
|
name=canonical,
|
|
@@ -333,19 +429,23 @@ def _build_artifact(
|
|
|
333
429
|
|
|
334
430
|
|
|
335
431
|
def _parse_upload_time(raw: str | None) -> datetime | None:
|
|
336
|
-
"""Parse an index ``upload-time`` string to a ``datetime``.
|
|
432
|
+
"""Parse an index ``upload-time`` string to a UTC ``datetime``.
|
|
337
433
|
|
|
338
434
|
Accepts the RFC 3339 form the Simple/JSON API serves (``Z`` or an
|
|
339
|
-
explicit offset)
|
|
340
|
-
|
|
341
|
-
|
|
435
|
+
explicit offset) and normalizes it to UTC (PEP 751 requires UTC for
|
|
436
|
+
the emitted field). Returns ``None`` when the field is absent,
|
|
437
|
+
unparseable, or has no timezone; the timestamp is informational, so
|
|
438
|
+
a bad value is dropped rather than fatal.
|
|
342
439
|
"""
|
|
343
440
|
if raw is None:
|
|
344
441
|
return None
|
|
345
442
|
try:
|
|
346
|
-
|
|
443
|
+
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
347
444
|
except ValueError:
|
|
348
445
|
return None
|
|
446
|
+
if parsed.tzinfo is None:
|
|
447
|
+
return None
|
|
448
|
+
return parsed.astimezone(timezone.utc)
|
|
349
449
|
|
|
350
450
|
|
|
351
451
|
def _filter_acceptable_hashes(
|
|
@@ -395,30 +495,58 @@ def _vcs_pin_from_source(
|
|
|
395
495
|
) -> VcsPin:
|
|
396
496
|
"""Build a :class:`VcsPin` from a :class:`VcsSource`.
|
|
397
497
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
498
|
+
``resolved_sha`` is the post-clone SHA recorded on the provider by
|
|
499
|
+
:func:`~nab_python._provider.sources.materialize_vcs_source`. A VCS
|
|
500
|
+
source cannot be pinned without first being materialised, so a
|
|
501
|
+
``None`` here is an internal invariant violation: raise
|
|
502
|
+
:class:`MissingVcsCommitError` rather than emit a branch name or
|
|
503
|
+
empty string as ``commit_id``.
|
|
402
504
|
|
|
403
505
|
``requested_revision`` is the URL's ``@<ref>``, kept only when it
|
|
404
506
|
is a named ref that differs from ``commit_id`` (i.e. the user did
|
|
405
|
-
not pin the bare SHA).
|
|
406
|
-
|
|
507
|
+
not pin the bare SHA). ``subdirectory`` carries the
|
|
508
|
+
``#subdirectory=`` fragment so an installer can locate the project
|
|
509
|
+
inside the repo.
|
|
510
|
+
|
|
511
|
+
``bare_repo_url`` comes from ``parsed.repo_url``, which
|
|
512
|
+
:meth:`VcsRequest.parse` has already separated from the ref and the
|
|
513
|
+
fragment. ``repo_url`` re-pins that bare URL to ``commit_id`` (the
|
|
514
|
+
``git+`` prefix, ``@<sha>``, and any ``#subdirectory=`` fragment) so
|
|
515
|
+
the requirements.txt line installs the locked commit, not the ref
|
|
516
|
+
the user supplied.
|
|
407
517
|
"""
|
|
408
|
-
from nab_index.vcs import
|
|
518
|
+
from nab_index.vcs import VcsRequest
|
|
409
519
|
|
|
410
520
|
from ..lockfile import VcsPin
|
|
411
521
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
522
|
+
if resolved_sha is None:
|
|
523
|
+
msg = (
|
|
524
|
+
f"{canonical}: VCS source pinned without a resolved commit SHA;"
|
|
525
|
+
" materialize_vcs_source records the post-clone SHA before any"
|
|
526
|
+
" version can be pinned, so this is an internal invariant"
|
|
527
|
+
" violation"
|
|
528
|
+
)
|
|
529
|
+
raise MissingVcsCommitError(msg)
|
|
530
|
+
parsed = VcsRequest.parse(source.url)
|
|
531
|
+
# Keep the named ref only when it differs from the SHA (tag or branch case).
|
|
532
|
+
requested_revision = (
|
|
533
|
+
parsed.ref if parsed.ref and parsed.ref != resolved_sha else None
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Compose a pinned installable URL from the parsed pieces, not from source.url,
|
|
537
|
+
# so credentials are stripped and the sha replaces any floating ref.
|
|
538
|
+
bare_repo_url = _strip_userinfo(parsed.repo_url)
|
|
539
|
+
repo_url = f"{parsed.scheme}+{bare_repo_url}@{resolved_sha}"
|
|
540
|
+
if parsed.subdirectory:
|
|
541
|
+
repo_url += f"#subdirectory={parsed.subdirectory}"
|
|
542
|
+
|
|
418
543
|
return VcsPin(
|
|
419
544
|
name=canonical,
|
|
420
545
|
version=str(version),
|
|
421
|
-
repo_url=
|
|
422
|
-
|
|
546
|
+
repo_url=repo_url,
|
|
547
|
+
bare_repo_url=bare_repo_url,
|
|
548
|
+
commit_id=resolved_sha,
|
|
549
|
+
subdirectory=parsed.subdirectory or None,
|
|
423
550
|
requested_revision=requested_revision,
|
|
551
|
+
vcs_type=parsed.scheme,
|
|
424
552
|
)
|