nab-python 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nab_python/__init__.py +1 -0
- nab_python/_build/__init__.py +1 -0
- nab_python/_build/env.py +364 -0
- nab_python/_build/errors.py +17 -0
- nab_python/_build/runner.py +254 -0
- nab_python/_lockfile/__init__.py +1 -0
- nab_python/_lockfile/builder.py +339 -0
- nab_python/_lockfile/disjointness.py +207 -0
- nab_python/_lockfile/pylock.py +323 -0
- nab_python/_lockfile/requirements.py +121 -0
- nab_python/_packaging_provider.py +98 -0
- nab_python/_provider/__init__.py +1 -0
- nab_python/_provider/build_remote.py +95 -0
- nab_python/_provider/extras.py +231 -0
- nab_python/_provider/listing.py +442 -0
- nab_python/_provider/lookahead.py +156 -0
- nab_python/_provider/metadata_resolver.py +450 -0
- nab_python/_provider/priority.py +174 -0
- nab_python/_provider/sources.py +215 -0
- nab_python/_testing/__init__.py +1 -0
- nab_python/_testing/coordinator_fake.py +240 -0
- nab_python/_vcs_admission.py +209 -0
- nab_python/_vendor/__init__.py +6 -0
- nab_python/_vendor/packaging/LICENSE +3 -0
- nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
- nab_python/_vendor/packaging/LICENSE.BSD +23 -0
- nab_python/_vendor/packaging/PROVENANCE.md +73 -0
- nab_python/_vendor/packaging/__init__.py +15 -0
- nab_python/_vendor/packaging/_elffile.py +108 -0
- nab_python/_vendor/packaging/_manylinux.py +265 -0
- nab_python/_vendor/packaging/_musllinux.py +88 -0
- nab_python/_vendor/packaging/_parser.py +394 -0
- nab_python/_vendor/packaging/_structures.py +33 -0
- nab_python/_vendor/packaging/_tokenizer.py +196 -0
- nab_python/_vendor/packaging/dependency_groups.py +302 -0
- nab_python/_vendor/packaging/direct_url.py +325 -0
- nab_python/_vendor/packaging/errors.py +94 -0
- nab_python/_vendor/packaging/licenses/__init__.py +186 -0
- nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
- nab_python/_vendor/packaging/markers.py +506 -0
- nab_python/_vendor/packaging/metadata.py +964 -0
- nab_python/_vendor/packaging/py.typed +0 -0
- nab_python/_vendor/packaging/pylock.py +910 -0
- nab_python/_vendor/packaging/ranges.py +1803 -0
- nab_python/_vendor/packaging/requirements.py +132 -0
- nab_python/_vendor/packaging/specifiers.py +1141 -0
- nab_python/_vendor/packaging/tags.py +929 -0
- nab_python/_vendor/packaging/utils.py +296 -0
- nab_python/_vendor/packaging/version.py +1230 -0
- nab_python/build_backend.py +184 -0
- nab_python/config.py +805 -0
- nab_python/download.py +170 -0
- nab_python/fetch.py +827 -0
- nab_python/lockfile.py +238 -0
- nab_python/metadata.py +145 -0
- nab_python/provider.py +1235 -0
- nab_python/py.typed +0 -0
- nab_python/requirements_file.py +180 -0
- nab_python/resolve.py +497 -0
- nab_python/universal/__init__.py +1 -0
- nab_python/universal/matrix.py +235 -0
- nab_python/universal/provider.py +214 -0
- nab_python/universal/reresolve.py +310 -0
- nab_python/universal/resolve.py +508 -0
- nab_python/universal/validate.py +439 -0
- nab_python/universal/wheel_selection.py +327 -0
- nab_python/workspace.py +214 -0
- nab_python-0.0.1.dist-info/METADATA +49 -0
- nab_python-0.0.1.dist-info/RECORD +71 -0
- nab_python-0.0.1.dist-info/WHEEL +4 -0
- nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""PEP 751 ``pylock.toml`` emission.
|
|
2
|
+
|
|
3
|
+
Owns ``write_lock`` and ``build_pylock`` plus the
|
|
4
|
+
:class:`LockInput` -> :class:`Pylock` shape conversion. The
|
|
5
|
+
per-tuple expansion path (when a universal resolve produced
|
|
6
|
+
different pins for different environments) collapses into one or
|
|
7
|
+
more ``Package`` entries with markers attached; the emit-time
|
|
8
|
+
disjointness validation lives in
|
|
9
|
+
:mod:`nab_python._lockfile.disjointness`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import tomli_w
|
|
19
|
+
|
|
20
|
+
from .._vendor.packaging.markers import Marker
|
|
21
|
+
from .._vendor.packaging.pylock import (
|
|
22
|
+
Package,
|
|
23
|
+
PackageDirectory,
|
|
24
|
+
PackageSdist,
|
|
25
|
+
PackageVcs,
|
|
26
|
+
PackageWheel,
|
|
27
|
+
Pylock,
|
|
28
|
+
)
|
|
29
|
+
from .._vendor.packaging.specifiers import SpecifierSet
|
|
30
|
+
from .._vendor.packaging.utils import canonicalize_name
|
|
31
|
+
from .._vendor.packaging.version import Version
|
|
32
|
+
from .disjointness import validate_marker_disjointness
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import os
|
|
36
|
+
from collections.abc import Mapping, Sequence
|
|
37
|
+
|
|
38
|
+
from ..lockfile import (
|
|
39
|
+
LockInput,
|
|
40
|
+
PinShape,
|
|
41
|
+
SdistArtifact,
|
|
42
|
+
WheelArtifact,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"build_pylock",
|
|
48
|
+
"write_lock",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def write_lock(
|
|
53
|
+
lock_input: LockInput,
|
|
54
|
+
*,
|
|
55
|
+
output_path: str | os.PathLike[str] | None = None,
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Serialise ``lock_input`` to PEP 751 TOML text.
|
|
58
|
+
|
|
59
|
+
Returns the TOML text. When ``output_path`` is provided, also
|
|
60
|
+
writes it atomically; the caller chooses the path (PEP 751 does
|
|
61
|
+
not mandate one).
|
|
62
|
+
"""
|
|
63
|
+
pylock = build_pylock(lock_input)
|
|
64
|
+
pylock.validate()
|
|
65
|
+
text = tomli_w.dumps(dict(pylock.to_dict()))
|
|
66
|
+
if output_path is not None:
|
|
67
|
+
Path(output_path).write_text(text, encoding="utf-8")
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_pylock(lock_input: LockInput) -> Pylock:
|
|
72
|
+
"""Build a :class:`Pylock` from the input shape.
|
|
73
|
+
|
|
74
|
+
The resolver-side data structures have already been simplified
|
|
75
|
+
when this function runs. The remaining work is shape conversion:
|
|
76
|
+
``Pin`` -> ``Package``, plus marker attachment from the per-tuple
|
|
77
|
+
map.
|
|
78
|
+
"""
|
|
79
|
+
from ..lockfile import LOCK_VERSION
|
|
80
|
+
|
|
81
|
+
if lock_input.per_tuple_pins:
|
|
82
|
+
package_records = _build_per_tuple_packages(lock_input)
|
|
83
|
+
else:
|
|
84
|
+
package_records = [_pin_to_package(pin) for pin in lock_input.pins.values()]
|
|
85
|
+
package_records.sort(key=_package_sort_key)
|
|
86
|
+
validate_marker_disjointness(
|
|
87
|
+
package_records,
|
|
88
|
+
environments=lock_input.tuple_environments,
|
|
89
|
+
extras=lock_input.extras,
|
|
90
|
+
groups=lock_input.dependency_groups,
|
|
91
|
+
)
|
|
92
|
+
tool: dict[str, Any] | None = (
|
|
93
|
+
{"nab": lock_input.provenance.to_block()}
|
|
94
|
+
if lock_input.provenance is not None
|
|
95
|
+
else None
|
|
96
|
+
)
|
|
97
|
+
return Pylock(
|
|
98
|
+
lock_version=Version(LOCK_VERSION),
|
|
99
|
+
environments=tuple(lock_input.environments) or None,
|
|
100
|
+
requires_python=(
|
|
101
|
+
SpecifierSet(lock_input.requires_python)
|
|
102
|
+
if lock_input.requires_python
|
|
103
|
+
else None
|
|
104
|
+
),
|
|
105
|
+
extras=(
|
|
106
|
+
tuple(canonicalize_name(e) for e in lock_input.extras)
|
|
107
|
+
if lock_input.extras
|
|
108
|
+
else None
|
|
109
|
+
),
|
|
110
|
+
dependency_groups=(
|
|
111
|
+
tuple(lock_input.dependency_groups)
|
|
112
|
+
if lock_input.dependency_groups
|
|
113
|
+
else None
|
|
114
|
+
),
|
|
115
|
+
default_groups=(
|
|
116
|
+
tuple(lock_input.default_groups) if lock_input.default_groups else None
|
|
117
|
+
),
|
|
118
|
+
created_by=lock_input.created_by,
|
|
119
|
+
packages=package_records,
|
|
120
|
+
tool=tool,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _pin_to_package(pin: PinShape, marker: Marker | None = None) -> Package:
|
|
125
|
+
from ..lockfile import IndexPin, LocalPin, VcsPin
|
|
126
|
+
|
|
127
|
+
if isinstance(pin, IndexPin):
|
|
128
|
+
return Package(
|
|
129
|
+
name=canonicalize_name(pin.name),
|
|
130
|
+
version=Version(pin.version),
|
|
131
|
+
marker=marker,
|
|
132
|
+
requires_python=(
|
|
133
|
+
SpecifierSet(pin.requires_python) if pin.requires_python else None
|
|
134
|
+
),
|
|
135
|
+
index=pin.index,
|
|
136
|
+
sdist=_sdist_to_package(pin.sdist) if pin.sdist else None,
|
|
137
|
+
wheels=tuple(_wheel_to_package(w) for w in pin.wheels) or None,
|
|
138
|
+
)
|
|
139
|
+
if isinstance(pin, LocalPin):
|
|
140
|
+
return Package(
|
|
141
|
+
name=canonicalize_name(pin.name),
|
|
142
|
+
version=Version(pin.version),
|
|
143
|
+
marker=marker,
|
|
144
|
+
directory=PackageDirectory(path=pin.path, editable=False),
|
|
145
|
+
)
|
|
146
|
+
if isinstance(pin, VcsPin):
|
|
147
|
+
return Package(
|
|
148
|
+
name=canonicalize_name(pin.name),
|
|
149
|
+
version=Version(pin.version),
|
|
150
|
+
marker=marker,
|
|
151
|
+
vcs=PackageVcs(
|
|
152
|
+
type="git",
|
|
153
|
+
url=pin.repo_url,
|
|
154
|
+
commit_id=pin.commit_id,
|
|
155
|
+
subdirectory=pin.subdirectory,
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
msg = f"unknown pin shape: {pin!r}"
|
|
159
|
+
raise TypeError(msg)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _wheel_to_package(wheel: WheelArtifact) -> PackageWheel:
|
|
163
|
+
return PackageWheel(
|
|
164
|
+
name=wheel.filename,
|
|
165
|
+
url=wheel.url,
|
|
166
|
+
size=wheel.size,
|
|
167
|
+
hashes=dict(wheel.hashes),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _sdist_to_package(sdist: SdistArtifact) -> PackageSdist:
|
|
172
|
+
return PackageSdist(
|
|
173
|
+
name=sdist.filename,
|
|
174
|
+
url=sdist.url,
|
|
175
|
+
size=sdist.size,
|
|
176
|
+
hashes=dict(sdist.hashes),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _build_per_tuple_packages(lock_input: LockInput) -> list[Package]:
|
|
181
|
+
"""Collapse per-tuple pins into Package entries with markers.
|
|
182
|
+
|
|
183
|
+
For each canonical package name:
|
|
184
|
+
* Group tuples by (version, source-shape).
|
|
185
|
+
* Emit a Package per group; the marker is the OR of the matching
|
|
186
|
+
tuples' markers. When the group's tuples cover the entire
|
|
187
|
+
declared universe (``lock_input.tuple_markers``) the package is
|
|
188
|
+
unconditional and the marker is omitted.
|
|
189
|
+
* Within a group, the artefact sets (wheels and sdist) are
|
|
190
|
+
unioned across the contributing tuples so tuple-specific
|
|
191
|
+
wheels (e.g. cp310-manylinux vs cp311-macos) survive.
|
|
192
|
+
|
|
193
|
+
The emitted marker is the raw OR of the per-tuple marker
|
|
194
|
+
expressions; no Boolean minimisation runs.
|
|
195
|
+
"""
|
|
196
|
+
out: list[Package] = []
|
|
197
|
+
by_name = _group_by_name(lock_input.per_tuple_pins)
|
|
198
|
+
total_tuples = len(lock_input.tuple_markers)
|
|
199
|
+
for per_tuple in by_name.values():
|
|
200
|
+
groups = _group_pins_by_pin(per_tuple)
|
|
201
|
+
for pins, tuple_labels in groups:
|
|
202
|
+
marker = _build_marker(tuple_labels, lock_input.tuple_markers, total_tuples)
|
|
203
|
+
out.append(_pin_to_package(_merge_pins_in_group(pins), marker))
|
|
204
|
+
# Pins only present in lock_input.pins (e.g. tuples agreed via the
|
|
205
|
+
# single-source path) emit unconditionally.
|
|
206
|
+
for canonical_name, pin in lock_input.pins.items():
|
|
207
|
+
if canonical_name not in by_name:
|
|
208
|
+
out.append(_pin_to_package(pin))
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _group_by_name(
|
|
213
|
+
per_tuple_pins: Mapping[str, Mapping[str, PinShape]],
|
|
214
|
+
) -> dict[str, dict[str, PinShape]]:
|
|
215
|
+
"""Pivot ``{tuple_label: {name: pin}}`` to ``{canonical: {label: pin}}``."""
|
|
216
|
+
out: defaultdict[str, dict[str, PinShape]] = defaultdict(dict)
|
|
217
|
+
for tuple_label, per_name in per_tuple_pins.items():
|
|
218
|
+
for raw_name, pin in per_name.items():
|
|
219
|
+
canonical = canonicalize_name(raw_name)
|
|
220
|
+
out[canonical][tuple_label] = pin
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _group_pins_by_pin(
|
|
225
|
+
per_tuple: dict[str, PinShape],
|
|
226
|
+
) -> list[tuple[list[PinShape], list[str]]]:
|
|
227
|
+
"""Bucket tuples by structural pin discriminator, keeping every pin."""
|
|
228
|
+
by_key: dict[tuple, tuple[list[PinShape], list[str]]] = {}
|
|
229
|
+
for tuple_label, pin in per_tuple.items():
|
|
230
|
+
key = _pin_discriminator(pin)
|
|
231
|
+
if key not in by_key:
|
|
232
|
+
by_key[key] = ([], [])
|
|
233
|
+
by_key[key][0].append(pin)
|
|
234
|
+
by_key[key][1].append(tuple_label)
|
|
235
|
+
return list(by_key.values())
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _pin_discriminator(pin: PinShape) -> tuple:
|
|
239
|
+
"""Return a hashable key that identifies the source + version of ``pin``."""
|
|
240
|
+
from ..lockfile import IndexPin, LocalPin, VcsPin
|
|
241
|
+
|
|
242
|
+
if isinstance(pin, IndexPin):
|
|
243
|
+
return ("index", pin.version, pin.index)
|
|
244
|
+
if isinstance(pin, LocalPin):
|
|
245
|
+
return ("local", pin.version, pin.path)
|
|
246
|
+
if isinstance(pin, VcsPin):
|
|
247
|
+
return ("vcs", pin.commit_id, pin.repo_url, pin.subdirectory or "")
|
|
248
|
+
msg = f"unknown pin shape: {pin!r}"
|
|
249
|
+
raise TypeError(msg)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _merge_pins_in_group(pins: list[PinShape]) -> PinShape:
|
|
253
|
+
"""Combine pins sharing a discriminator into one with unioned artefacts.
|
|
254
|
+
|
|
255
|
+
For :class:`IndexPin`, accumulates every distinct wheel filename
|
|
256
|
+
across the contributing tuples and keeps the first non-``None``
|
|
257
|
+
sdist. ``requires_python`` survives only when every tuple agreed,
|
|
258
|
+
matching :func:`_common_requires_python`'s rule.
|
|
259
|
+
Non-IndexPin shapes are already fully discriminated, so the first
|
|
260
|
+
pin is returned unchanged.
|
|
261
|
+
"""
|
|
262
|
+
from ..lockfile import IndexPin
|
|
263
|
+
|
|
264
|
+
head = pins[0]
|
|
265
|
+
if not isinstance(head, IndexPin):
|
|
266
|
+
return head
|
|
267
|
+
seen_wheels: dict[str, WheelArtifact] = {}
|
|
268
|
+
sdist = head.sdist
|
|
269
|
+
requires_python_set: set[str] = set()
|
|
270
|
+
for pin in pins:
|
|
271
|
+
assert isinstance(pin, IndexPin)
|
|
272
|
+
for wheel in pin.wheels:
|
|
273
|
+
seen_wheels.setdefault(wheel.filename, wheel)
|
|
274
|
+
if sdist is None and pin.sdist is not None:
|
|
275
|
+
sdist = pin.sdist
|
|
276
|
+
if pin.requires_python is not None:
|
|
277
|
+
requires_python_set.add(pin.requires_python)
|
|
278
|
+
requires_python = (
|
|
279
|
+
next(iter(requires_python_set)) if len(requires_python_set) == 1 else None
|
|
280
|
+
)
|
|
281
|
+
return IndexPin(
|
|
282
|
+
name=head.name,
|
|
283
|
+
version=head.version,
|
|
284
|
+
index=head.index,
|
|
285
|
+
sdist=sdist,
|
|
286
|
+
wheels=tuple(seen_wheels.values()),
|
|
287
|
+
requires_python=requires_python,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _build_marker(
|
|
292
|
+
tuple_labels: Sequence[str],
|
|
293
|
+
tuple_markers: Mapping[str, Marker],
|
|
294
|
+
total_tuples: int,
|
|
295
|
+
) -> Marker | None:
|
|
296
|
+
"""Return the marker selecting ``tuple_labels``, or ``None`` if unconditional.
|
|
297
|
+
|
|
298
|
+
The package is unconditional when ``tuple_labels`` covers every
|
|
299
|
+
declared tuple in ``tuple_markers``. Otherwise the marker is the
|
|
300
|
+
OR of the per-tuple markers. When ``tuple_markers`` is empty the
|
|
301
|
+
caller has not declared a tuple universe and we omit the marker.
|
|
302
|
+
"""
|
|
303
|
+
if total_tuples == 0 or len(tuple_labels) >= total_tuples:
|
|
304
|
+
return None
|
|
305
|
+
markers = [tuple_markers[label] for label in tuple_labels if label in tuple_markers]
|
|
306
|
+
if not markers:
|
|
307
|
+
return None
|
|
308
|
+
return _or_markers(markers)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _or_markers(markers: Sequence[Marker]) -> Marker:
|
|
312
|
+
"""Return a Marker that evaluates True if any of ``markers`` does."""
|
|
313
|
+
if not markers:
|
|
314
|
+
msg = "_or_markers requires at least one marker"
|
|
315
|
+
raise ValueError(msg)
|
|
316
|
+
if len(markers) == 1:
|
|
317
|
+
return markers[0]
|
|
318
|
+
parts = [f"({m})" for m in markers]
|
|
319
|
+
return Marker(" or ".join(parts))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _package_sort_key(package: Package) -> tuple:
|
|
323
|
+
return (str(package.name), str(package.version) if package.version else "")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Pip-compatible ``requirements.txt`` rendering for a finished resolve.
|
|
2
|
+
|
|
3
|
+
Produces text that pip's hash-checking mode can install (with
|
|
4
|
+
``--hash=sha256:...`` lines) or a plain ``name==version`` list when
|
|
5
|
+
hashes are not required. Per-tuple resolves render as commented
|
|
6
|
+
sections; pip cannot install a single requirements.txt across
|
|
7
|
+
multiple ``(python, platform)`` tuples in hash-checking mode.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import os
|
|
17
|
+
from collections.abc import Mapping
|
|
18
|
+
|
|
19
|
+
from ..lockfile import IndexPin, LockInput, PinShape
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"write_requirements_with_hashes",
|
|
24
|
+
"write_requirements_without_hashes",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_requirements_with_hashes(
|
|
29
|
+
lock_input: LockInput, *, output_path: str | os.PathLike[str] | None = None
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Render ``lock_input`` as a pip-compatible requirements.txt.
|
|
32
|
+
|
|
33
|
+
Each line is ``name==version`` followed by one ``--hash=sha256:...``
|
|
34
|
+
per recorded artefact, in the format pip's hash-checking mode
|
|
35
|
+
accepts. Local and VCS pins are emitted as ``name @ <url>`` lines
|
|
36
|
+
without hashes (pip does not hash-check those forms). Returns the
|
|
37
|
+
text and, when ``output_path`` is provided, atomically writes it.
|
|
38
|
+
"""
|
|
39
|
+
return _render_requirements(lock_input, with_hashes=True, output_path=output_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_requirements_without_hashes(
|
|
43
|
+
lock_input: LockInput, *, output_path: str | os.PathLike[str] | None = None
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Render ``lock_input`` as a plain ``name==version`` list.
|
|
46
|
+
|
|
47
|
+
Same shape as :func:`write_requirements_with_hashes` but without
|
|
48
|
+
the ``--hash=sha256:...`` lines. Local and VCS pins still render
|
|
49
|
+
as ``name @ <url>``. Returns the text and, when ``output_path``
|
|
50
|
+
is provided, atomically writes it.
|
|
51
|
+
"""
|
|
52
|
+
return _render_requirements(lock_input, with_hashes=False, output_path=output_path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _render_requirements(
|
|
56
|
+
lock_input: LockInput,
|
|
57
|
+
*,
|
|
58
|
+
with_hashes: bool,
|
|
59
|
+
output_path: str | os.PathLike[str] | None,
|
|
60
|
+
) -> str:
|
|
61
|
+
if lock_input.per_tuple_pins:
|
|
62
|
+
text = _render_per_tuple_requirements(lock_input, with_hashes=with_hashes)
|
|
63
|
+
else:
|
|
64
|
+
lines = _render_pins(lock_input.pins, with_hashes=with_hashes)
|
|
65
|
+
text = "\n".join(lines) + "\n"
|
|
66
|
+
if output_path is not None:
|
|
67
|
+
Path(output_path).write_text(text, encoding="utf-8")
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _render_per_tuple_requirements(lock_input: LockInput, *, with_hashes: bool) -> str:
|
|
72
|
+
"""Emit one ``# label`` block per tuple followed by that tuple's pins.
|
|
73
|
+
|
|
74
|
+
Pip cannot install a single requirements.txt across multiple
|
|
75
|
+
``(python, platform)`` tuples in hash-checking mode, so a multi-
|
|
76
|
+
tuple resolve serialises as commented sections that callers are
|
|
77
|
+
expected to extract per environment.
|
|
78
|
+
"""
|
|
79
|
+
blocks: list[str] = []
|
|
80
|
+
for label in lock_input.per_tuple_pins:
|
|
81
|
+
pins = lock_input.per_tuple_pins[label]
|
|
82
|
+
block = [f"# {label}"]
|
|
83
|
+
block.extend(_render_pins(pins, with_hashes=with_hashes))
|
|
84
|
+
blocks.append("\n".join(block))
|
|
85
|
+
return "\n\n".join(blocks) + "\n"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_pins(pins: Mapping[str, PinShape], *, with_hashes: bool) -> list[str]:
|
|
89
|
+
"""Render a flat ``{name: pin}`` mapping in alphabetical order."""
|
|
90
|
+
from ..lockfile import IndexPin, LocalPin, VcsPin
|
|
91
|
+
|
|
92
|
+
lines: list[str] = []
|
|
93
|
+
for canonical in sorted(pins):
|
|
94
|
+
pin = pins[canonical]
|
|
95
|
+
|
|
96
|
+
if isinstance(pin, IndexPin):
|
|
97
|
+
lines.extend(_render_index_pin(pin, with_hashes=with_hashes))
|
|
98
|
+
elif isinstance(pin, LocalPin):
|
|
99
|
+
lines.append(f"{pin.name} @ {Path(pin.path).resolve().as_uri()}")
|
|
100
|
+
elif isinstance(pin, VcsPin):
|
|
101
|
+
lines.append(f"{pin.name} @ {pin.repo_url}")
|
|
102
|
+
else: # pragma: no cover - exhaustive
|
|
103
|
+
msg = f"unknown pin shape: {pin!r}"
|
|
104
|
+
raise TypeError(msg)
|
|
105
|
+
|
|
106
|
+
return lines
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_index_pin(pin: IndexPin, *, with_hashes: bool = True) -> list[str]:
|
|
110
|
+
if not with_hashes:
|
|
111
|
+
return [f"{pin.name}=={pin.version}"]
|
|
112
|
+
digests: list[tuple[str, str]] = []
|
|
113
|
+
if pin.sdist is not None:
|
|
114
|
+
digests.extend(pin.sdist.hashes)
|
|
115
|
+
for wheel in pin.wheels:
|
|
116
|
+
digests.extend(wheel.hashes)
|
|
117
|
+
if not digests:
|
|
118
|
+
return [f"{pin.name}=={pin.version}"]
|
|
119
|
+
parts = [f"{pin.name}=={pin.version}"]
|
|
120
|
+
parts.extend(f"--hash={algo}:{d}" for algo, d in digests)
|
|
121
|
+
return [" \\\n ".join(parts)]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Provider that bridges packaging's PEP 440 types to nab-resolver."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
|
|
10
|
+
from nab_resolver.types import Incompatibility, RangeProtocol
|
|
11
|
+
|
|
12
|
+
from ._vendor.packaging.ranges import VersionRange
|
|
13
|
+
from ._vendor.packaging.specifiers import SpecifierSet
|
|
14
|
+
from ._vendor.packaging.version import Version
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"PackagingProvider",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PackagingProvider:
|
|
22
|
+
"""In-memory provider using PEP 440 versions.
|
|
23
|
+
|
|
24
|
+
Packages are strings, versions are :class:`packaging.version.Version`,
|
|
25
|
+
and dependency constraints are :class:`packaging.specifiers.SpecifierSet`,
|
|
26
|
+
converted to :class:`packaging.ranges.VersionRange` for the resolver.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
packages: dict[str, dict[Version, dict[str, SpecifierSet]]],
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Create a provider from a package graph."""
|
|
34
|
+
self._packages = packages
|
|
35
|
+
|
|
36
|
+
def _get_versions(self, package: str) -> list[Version]:
|
|
37
|
+
if package not in self._packages:
|
|
38
|
+
return []
|
|
39
|
+
return sorted(self._packages[package].keys(), reverse=True)
|
|
40
|
+
|
|
41
|
+
def choose_version(
|
|
42
|
+
self, package: str, version_range: RangeProtocol[Version]
|
|
43
|
+
) -> Version | None:
|
|
44
|
+
"""Pick the newest version within the allowed range."""
|
|
45
|
+
for version in self._get_versions(package):
|
|
46
|
+
if version in version_range:
|
|
47
|
+
return version
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def get_dependencies(
|
|
51
|
+
self, package: str, version: Version
|
|
52
|
+
) -> dict[str, VersionRange]:
|
|
53
|
+
"""Convert SpecifierSet deps to VersionRange deps."""
|
|
54
|
+
raw = self._packages.get(package, {}).get(version, {})
|
|
55
|
+
return {dep: spec.to_range() for dep, spec in raw.items()}
|
|
56
|
+
|
|
57
|
+
_CONFLICT_THRESHOLD = 5
|
|
58
|
+
|
|
59
|
+
def prioritize(
|
|
60
|
+
self,
|
|
61
|
+
package: str,
|
|
62
|
+
version_range: RangeProtocol[Version],
|
|
63
|
+
conflict_counts: Mapping[str, int],
|
|
64
|
+
culprit_counts: Mapping[str, int] | None = None,
|
|
65
|
+
) -> tuple[bool, int]:
|
|
66
|
+
"""Prioritize packages for resolution order.
|
|
67
|
+
|
|
68
|
+
Returns a tuple compared with min(), so lower = decided first.
|
|
69
|
+
Packages with many conflicts are promoted so the resolver
|
|
70
|
+
discovers incompatibilities before deciding downstream packages.
|
|
71
|
+
``culprit_counts`` is accepted for protocol compatibility but
|
|
72
|
+
not used by this provider.
|
|
73
|
+
"""
|
|
74
|
+
del culprit_counts
|
|
75
|
+
promoted = conflict_counts.get(package, 0) >= self._CONFLICT_THRESHOLD
|
|
76
|
+
versions = self._get_versions(package)
|
|
77
|
+
matching = sum(1 for v in versions if v in version_range)
|
|
78
|
+
return (not promoted, matching)
|
|
79
|
+
|
|
80
|
+
def is_ready(self, package: str) -> bool:
|
|
81
|
+
"""All packages are immediately decidable for this in-memory provider."""
|
|
82
|
+
del package
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def receive_partial_solution_hint(
|
|
86
|
+
self,
|
|
87
|
+
positive_ranges: Mapping[str, RangeProtocol[Version]],
|
|
88
|
+
decisions: Mapping[str, Version],
|
|
89
|
+
) -> None:
|
|
90
|
+
"""No-op: in-memory provider does not use partial solution state."""
|
|
91
|
+
|
|
92
|
+
def consume_pending_clauses(self) -> list[Incompatibility[str, Version]]:
|
|
93
|
+
"""No queued clauses for this in-memory provider."""
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
def consume_force_backtrack_targets(self) -> list[str]:
|
|
97
|
+
"""No force-backtrack signal from this in-memory provider."""
|
|
98
|
+
return []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal helpers backing :mod:`nab_python.provider`."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""BUILD_REMOTE path: fetch, extract, and build a remote sdist.
|
|
2
|
+
|
|
3
|
+
Invoked from :func:`resolve_dynamic_sdist` when neither the
|
|
4
|
+
:pep:`643` static-deps path nor the bundled ``pyproject.toml``
|
|
5
|
+
fallback yields usable dependency metadata, and the effective
|
|
6
|
+
:class:`~nab_python.provider.BuildPolicy` for the package is
|
|
7
|
+
:attr:`~nab_python.provider.BuildPolicy.BUILD_REMOTE`.
|
|
8
|
+
|
|
9
|
+
A failure here raises :class:`~nab_python.provider.UnsupportedSdistError`
|
|
10
|
+
so :func:`nab_python._provider.lookahead.look_ahead_ok` can skip the
|
|
11
|
+
version. If every candidate fails the resolver surfaces the
|
|
12
|
+
accumulated reasons as a no-version-satisfies error: invoking
|
|
13
|
+
``BUILD_REMOTE`` does not turn a broken sdist into a usable one,
|
|
14
|
+
it just turns silence into a real diagnostic.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from nab_index.client import SdistFile, WheelFile, extract_sdist_archive
|
|
24
|
+
|
|
25
|
+
from .._vendor.packaging.utils import canonicalize_name
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
|
|
30
|
+
from .._vendor.packaging.version import Version
|
|
31
|
+
from ..metadata import WheelMetadata
|
|
32
|
+
from ..provider import Provider
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_remote_sdist(
|
|
36
|
+
provider: Provider,
|
|
37
|
+
package: str,
|
|
38
|
+
version: Version,
|
|
39
|
+
) -> WheelMetadata:
|
|
40
|
+
"""Download the sdist for ``(package, version)``, extract, and build.
|
|
41
|
+
|
|
42
|
+
``package`` is the canonical package name; ``version`` matches an
|
|
43
|
+
entry in ``provider.versions_cache``.
|
|
44
|
+
"""
|
|
45
|
+
# Late imports: ``provider`` imports this module at module load.
|
|
46
|
+
from .. import build_backend
|
|
47
|
+
from ..build_backend import BuildBackendError
|
|
48
|
+
from ..provider import UnsupportedSdistError
|
|
49
|
+
|
|
50
|
+
canonical = canonicalize_name(package)
|
|
51
|
+
versions = provider.versions_cache.get(canonical, [])
|
|
52
|
+
sdist = _find_sdist(versions, version)
|
|
53
|
+
if sdist is None:
|
|
54
|
+
msg = (
|
|
55
|
+
f"{package}=={version} build-remote requested but no sdist"
|
|
56
|
+
" is available in the listing"
|
|
57
|
+
)
|
|
58
|
+
raise UnsupportedSdistError(msg)
|
|
59
|
+
|
|
60
|
+
ver_str = str(version)
|
|
61
|
+
event = provider.coordinator.request_sdist_archive(canonical, ver_str, sdist.url)
|
|
62
|
+
event.wait()
|
|
63
|
+
data = provider.coordinator.index.get_sdist_archive(canonical, ver_str)
|
|
64
|
+
if data is None:
|
|
65
|
+
msg = (
|
|
66
|
+
f"{package}=={version} build-remote requested but sdist archive"
|
|
67
|
+
f" fetch from {sdist.url} failed"
|
|
68
|
+
)
|
|
69
|
+
raise UnsupportedSdistError(msg)
|
|
70
|
+
|
|
71
|
+
with tempfile.TemporaryDirectory(prefix="nab-build-remote-") as td:
|
|
72
|
+
try:
|
|
73
|
+
source_dir = extract_sdist_archive(data, Path(td))
|
|
74
|
+
except (OSError, ValueError) as exc:
|
|
75
|
+
msg = f"{package}=={version} sdist archive could not be extracted: {exc}"
|
|
76
|
+
raise UnsupportedSdistError(msg) from exc
|
|
77
|
+
try:
|
|
78
|
+
return build_backend.extract_metadata(
|
|
79
|
+
source_dir,
|
|
80
|
+
config=provider.build_config,
|
|
81
|
+
python_version=provider.python_version,
|
|
82
|
+
)
|
|
83
|
+
except BuildBackendError as exc:
|
|
84
|
+
msg = f"{package}=={version} build-remote backend failed: {exc}"
|
|
85
|
+
raise UnsupportedSdistError(msg) from exc
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _find_sdist(
|
|
89
|
+
versions: Sequence[tuple[Version, WheelFile | SdistFile]],
|
|
90
|
+
version: Version,
|
|
91
|
+
) -> SdistFile | None:
|
|
92
|
+
for v, d in versions:
|
|
93
|
+
if v == version and isinstance(d, SdistFile):
|
|
94
|
+
return d
|
|
95
|
+
return None
|