nab-python 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nab_python/_build/env.py +1 -1
- nab_python/_lockfile/builder.py +78 -11
- nab_python/_lockfile/pylock.py +108 -19
- nab_python/_provider/metadata_resolver.py +7 -3
- nab_python/_provider/sources.py +3 -0
- nab_python/_testing/coordinator_fake.py +5 -2
- nab_python/_vendor/packaging/PROVENANCE.md +2 -2
- nab_python/_vendor/packaging/_range_utils.py +773 -0
- nab_python/_vendor/packaging/_version_utils.py +37 -0
- nab_python/_vendor/packaging/ranges.py +160 -820
- nab_python/_vendor/packaging/specifiers.py +225 -42
- nab_python/config.py +42 -2
- nab_python/lockfile.py +32 -2
- nab_python/provider.py +5 -0
- nab_python/requirements_file.py +29 -0
- nab_python/resolve.py +20 -5
- nab_python/universal/resolve.py +39 -5
- {nab_python-0.0.2.dist-info → nab_python-0.0.3.dist-info}/METADATA +4 -5
- {nab_python-0.0.2.dist-info → nab_python-0.0.3.dist-info}/RECORD +20 -19
- nab_python-0.0.2.dist-info/licenses/LICENSE +0 -21
- {nab_python-0.0.2.dist-info → nab_python-0.0.3.dist-info}/WHEEL +0 -0
nab_python/_build/env.py
CHANGED
|
@@ -314,7 +314,7 @@ def _venv_python(venv_path: Path) -> Path:
|
|
|
314
314
|
r"""Return the venv interpreter path (``Scripts\python.exe`` on Windows)."""
|
|
315
315
|
if sys.platform == "win32":
|
|
316
316
|
return venv_path / "Scripts" / "python.exe" # pragma: no cover
|
|
317
|
-
return venv_path / "bin" / "python"
|
|
317
|
+
return venv_path / "bin" / "python" # pragma: no cover
|
|
318
318
|
|
|
319
319
|
|
|
320
320
|
def _venv_scheme_paths(python_executable: Path) -> dict[str, str]:
|
nab_python/_lockfile/builder.py
CHANGED
|
@@ -12,11 +12,13 @@ from __future__ import annotations
|
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import TYPE_CHECKING, Protocol, overload
|
|
15
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
15
16
|
|
|
16
17
|
import tomli
|
|
17
18
|
|
|
18
19
|
from nab_index.client import SdistFile, WheelFile
|
|
19
20
|
|
|
21
|
+
from .._vendor.packaging.pylock import Pylock, PylockValidationError
|
|
20
22
|
from .._vendor.packaging.utils import canonicalize_name
|
|
21
23
|
|
|
22
24
|
if TYPE_CHECKING:
|
|
@@ -40,6 +42,7 @@ __all__ = [
|
|
|
40
42
|
"MissingHashError",
|
|
41
43
|
"build_lock_input_from_provider",
|
|
42
44
|
"read_lockfile_anchor",
|
|
45
|
+
"read_lockfile_packages",
|
|
43
46
|
]
|
|
44
47
|
|
|
45
48
|
|
|
@@ -132,14 +135,54 @@ def read_lockfile_anchor(path: Path) -> datetime | None:
|
|
|
132
135
|
if isinstance(raw, datetime):
|
|
133
136
|
return raw if raw.tzinfo else raw.replace(tzinfo=timezone.utc)
|
|
134
137
|
if isinstance(raw, str):
|
|
138
|
+
# Python 3.10's fromisoformat rejects a trailing 'Z'; 3.11+ accept it.
|
|
139
|
+
iso = raw[:-1] + "+00:00" if raw.endswith("Z") else raw
|
|
135
140
|
try:
|
|
136
|
-
dt = datetime.fromisoformat(
|
|
141
|
+
dt = datetime.fromisoformat(iso)
|
|
137
142
|
except ValueError:
|
|
138
143
|
return None
|
|
139
144
|
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
140
145
|
return None
|
|
141
146
|
|
|
142
147
|
|
|
148
|
+
def read_lockfile_packages(path: Path) -> dict[str, Version] | None:
|
|
149
|
+
"""Return the ``name -> version`` map from a prior pylock at ``path``.
|
|
150
|
+
|
|
151
|
+
Used by ``nab lock`` to diff a re-lock against the previous result.
|
|
152
|
+
Packages without a recorded version (direct-reference entries that
|
|
153
|
+
omit it) are skipped.
|
|
154
|
+
|
|
155
|
+
Returns ``None`` when ``path`` does not exist, is not valid TOML,
|
|
156
|
+
or is not a spec-compliant PEP 751 lockfile; the caller falls back
|
|
157
|
+
to a no-diff summary line.
|
|
158
|
+
"""
|
|
159
|
+
if not path.is_file():
|
|
160
|
+
return None
|
|
161
|
+
try:
|
|
162
|
+
with path.open("rb") as f:
|
|
163
|
+
data = tomli.load(f)
|
|
164
|
+
pylock = Pylock.from_dict(data)
|
|
165
|
+
except (OSError, tomli.TOMLDecodeError, PylockValidationError):
|
|
166
|
+
return None
|
|
167
|
+
return {
|
|
168
|
+
str(pkg.name): pkg.version for pkg in pylock.packages if pkg.version is not None
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _strip_userinfo(url: str) -> str:
|
|
173
|
+
"""Return ``url`` with any ``user:password@`` userinfo removed.
|
|
174
|
+
|
|
175
|
+
Lockfiles are committed to version control, so an index or VCS
|
|
176
|
+
URL carrying embedded credentials must not be written verbatim.
|
|
177
|
+
Only the userinfo is dropped: host case and port are preserved
|
|
178
|
+
and a ``git+`` scheme prefix is left intact. A no-op for URLs
|
|
179
|
+
without credentials.
|
|
180
|
+
"""
|
|
181
|
+
parts = urlsplit(url)
|
|
182
|
+
netloc = parts.netloc.rpartition("@")[2]
|
|
183
|
+
return urlunsplit(parts._replace(netloc=netloc))
|
|
184
|
+
|
|
185
|
+
|
|
143
186
|
def build_lock_input_from_provider(
|
|
144
187
|
provider: LockInputProvider,
|
|
145
188
|
pins: Mapping[str, Version],
|
|
@@ -176,6 +219,8 @@ def build_lock_input_from_provider(
|
|
|
176
219
|
name=canonical,
|
|
177
220
|
version=str(version),
|
|
178
221
|
path=str(Path(local_source.path).resolve()),
|
|
222
|
+
editable=local_source.editable,
|
|
223
|
+
subdirectory=local_source.subdirectory,
|
|
179
224
|
)
|
|
180
225
|
continue
|
|
181
226
|
vcs_source = provider.vcs_source_for(canonical)
|
|
@@ -252,7 +297,7 @@ def _index_pin_from_listing(
|
|
|
252
297
|
return IndexPin(
|
|
253
298
|
name=canonical,
|
|
254
299
|
version=str(version),
|
|
255
|
-
index=index_url,
|
|
300
|
+
index=_strip_userinfo(index_url),
|
|
256
301
|
sdist=sdist,
|
|
257
302
|
wheels=wheels,
|
|
258
303
|
requires_python=requires_python,
|
|
@@ -282,9 +327,27 @@ def _build_artifact(
|
|
|
282
327
|
url=source.url,
|
|
283
328
|
hashes=hashes,
|
|
284
329
|
size=source.size,
|
|
330
|
+
upload_time=_parse_upload_time(source.upload_time),
|
|
331
|
+
local_path=source.local_path,
|
|
285
332
|
)
|
|
286
333
|
|
|
287
334
|
|
|
335
|
+
def _parse_upload_time(raw: str | None) -> datetime | None:
|
|
336
|
+
"""Parse an index ``upload-time`` string to a ``datetime``.
|
|
337
|
+
|
|
338
|
+
Accepts the RFC 3339 form the Simple/JSON API serves (``Z`` or an
|
|
339
|
+
explicit offset). Returns ``None`` when the field is absent or
|
|
340
|
+
unparseable; the timestamp is informational, so a bad value is
|
|
341
|
+
dropped rather than fatal.
|
|
342
|
+
"""
|
|
343
|
+
if raw is None:
|
|
344
|
+
return None
|
|
345
|
+
try:
|
|
346
|
+
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
347
|
+
except ValueError:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
288
351
|
def _filter_acceptable_hashes(
|
|
289
352
|
canonical: str, filename: str, hashes: tuple[tuple[str, str], ...]
|
|
290
353
|
) -> tuple[tuple[str, str], ...]:
|
|
@@ -336,22 +399,26 @@ def _vcs_pin_from_source(
|
|
|
336
399
|
provider) over the URL's ``@<ref>``: annotated tags and floating
|
|
337
400
|
refs only resolve to a commit after the clone runs. Fall back to
|
|
338
401
|
the URL ref when the source was never materialised.
|
|
402
|
+
|
|
403
|
+
``requested_revision`` is the URL's ``@<ref>``, kept only when it
|
|
404
|
+
is a named ref that differs from ``commit_id`` (i.e. the user did
|
|
405
|
+
not pin the bare SHA). An absent or unparseable ref leaves it
|
|
406
|
+
``None``.
|
|
339
407
|
"""
|
|
340
408
|
from nab_index.vcs import VcsCloneError, VcsRequest
|
|
341
409
|
|
|
342
410
|
from ..lockfile import VcsPin
|
|
343
411
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
except VcsCloneError:
|
|
351
|
-
commit_id = ""
|
|
412
|
+
try:
|
|
413
|
+
url_ref = VcsRequest.parse(source.url).ref
|
|
414
|
+
except VcsCloneError:
|
|
415
|
+
url_ref = ""
|
|
416
|
+
commit_id = resolved_sha if resolved_sha is not None else url_ref
|
|
417
|
+
requested_revision = url_ref if url_ref and url_ref != commit_id else None
|
|
352
418
|
return VcsPin(
|
|
353
419
|
name=canonical,
|
|
354
420
|
version=str(version),
|
|
355
|
-
repo_url=source.url,
|
|
421
|
+
repo_url=_strip_userinfo(source.url),
|
|
356
422
|
commit_id=commit_id,
|
|
423
|
+
requested_revision=requested_revision,
|
|
357
424
|
)
|
nab_python/_lockfile/pylock.py
CHANGED
|
@@ -11,6 +11,7 @@ disjointness validation lives in
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import os
|
|
14
15
|
from collections import defaultdict
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import TYPE_CHECKING, Any
|
|
@@ -32,7 +33,6 @@ from .._vendor.packaging.version import Version
|
|
|
32
33
|
from .disjointness import validate_marker_disjointness
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
|
-
import os
|
|
36
36
|
from collections.abc import Mapping, Sequence
|
|
37
37
|
|
|
38
38
|
from ..lockfile import (
|
|
@@ -57,10 +57,16 @@ def write_lock(
|
|
|
57
57
|
"""Serialise ``lock_input`` to PEP 751 TOML text.
|
|
58
58
|
|
|
59
59
|
Returns the TOML text. When ``output_path`` is provided, also
|
|
60
|
-
writes it
|
|
61
|
-
|
|
60
|
+
writes it there; the caller chooses the path (PEP 751 does not
|
|
61
|
+
mandate one).
|
|
62
|
+
|
|
63
|
+
Directory, wheel and sdist paths are written relative to
|
|
64
|
+
``output_path``'s parent so the lockfile stays portable between
|
|
65
|
+
machines (PEP 751 records those paths relative to the lock file).
|
|
66
|
+
With no ``output_path`` the current directory is the base.
|
|
62
67
|
"""
|
|
63
|
-
|
|
68
|
+
lock_dir = Path(output_path).parent if output_path is not None else None
|
|
69
|
+
pylock = build_pylock(lock_input, lock_dir=lock_dir)
|
|
64
70
|
pylock.validate()
|
|
65
71
|
text = tomli_w.dumps(dict(pylock.to_dict()))
|
|
66
72
|
if output_path is not None:
|
|
@@ -68,20 +74,28 @@ def write_lock(
|
|
|
68
74
|
return text
|
|
69
75
|
|
|
70
76
|
|
|
71
|
-
def build_pylock(lock_input: LockInput) -> Pylock:
|
|
77
|
+
def build_pylock(lock_input: LockInput, *, lock_dir: Path | None = None) -> Pylock:
|
|
72
78
|
"""Build a :class:`Pylock` from the input shape.
|
|
73
79
|
|
|
74
80
|
The resolver-side data structures have already been simplified
|
|
75
81
|
when this function runs. The remaining work is shape conversion:
|
|
76
82
|
``Pin`` -> ``Package``, plus marker attachment from the per-tuple
|
|
77
83
|
map.
|
|
84
|
+
|
|
85
|
+
``lock_dir`` is the directory the lockfile will be written to;
|
|
86
|
+
local-directory, wheel and sdist paths are emitted relative to it
|
|
87
|
+
so the lockfile is portable. It defaults to the current working
|
|
88
|
+
directory when the caller has no path in mind (e.g. stdout).
|
|
78
89
|
"""
|
|
79
90
|
from ..lockfile import LOCK_VERSION
|
|
80
91
|
|
|
92
|
+
base = (lock_dir if lock_dir is not None else Path.cwd()).resolve()
|
|
81
93
|
if lock_input.per_tuple_pins:
|
|
82
|
-
package_records = _build_per_tuple_packages(lock_input)
|
|
94
|
+
package_records = _build_per_tuple_packages(lock_input, base)
|
|
83
95
|
else:
|
|
84
|
-
package_records = [
|
|
96
|
+
package_records = [
|
|
97
|
+
_pin_to_package(pin, lock_dir=base) for pin in lock_input.pins.values()
|
|
98
|
+
]
|
|
85
99
|
package_records.sort(key=_package_sort_key)
|
|
86
100
|
validate_marker_disjointness(
|
|
87
101
|
package_records,
|
|
@@ -121,7 +135,29 @@ def build_pylock(lock_input: LockInput) -> Pylock:
|
|
|
121
135
|
)
|
|
122
136
|
|
|
123
137
|
|
|
124
|
-
def
|
|
138
|
+
def _relativize_path(target: str | os.PathLike[str], lock_dir: Path) -> str:
|
|
139
|
+
"""Return ``target`` as a POSIX path relative to ``lock_dir``.
|
|
140
|
+
|
|
141
|
+
PEP 751 records ``packages.directory.path`` and the wheel/sdist
|
|
142
|
+
``path`` fields relative to the lock file so the lockfile stays
|
|
143
|
+
portable between machines. :func:`os.path.relpath` is used
|
|
144
|
+
rather than :meth:`pathlib.Path.relative_to` so a ``target``
|
|
145
|
+
outside ``lock_dir`` still resolves, to a ``../``-prefixed path.
|
|
146
|
+
The result uses POSIX separators, which the spec recommends for
|
|
147
|
+
portable relative paths.
|
|
148
|
+
|
|
149
|
+
A Windows cross-drive ValueError falls back to the absolute path.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
rel = os.path.relpath(target, lock_dir)
|
|
153
|
+
except ValueError:
|
|
154
|
+
rel = os.fspath(target)
|
|
155
|
+
return Path(rel).as_posix()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _pin_to_package(
|
|
159
|
+
pin: PinShape, marker: Marker | None = None, *, lock_dir: Path
|
|
160
|
+
) -> Package:
|
|
125
161
|
from ..lockfile import IndexPin, LocalPin, VcsPin
|
|
126
162
|
|
|
127
163
|
if isinstance(pin, IndexPin):
|
|
@@ -133,51 +169,92 @@ def _pin_to_package(pin: PinShape, marker: Marker | None = None) -> Package:
|
|
|
133
169
|
SpecifierSet(pin.requires_python) if pin.requires_python else None
|
|
134
170
|
),
|
|
135
171
|
index=pin.index,
|
|
136
|
-
sdist=
|
|
137
|
-
|
|
172
|
+
sdist=(
|
|
173
|
+
_sdist_to_package(pin.sdist, lock_dir=lock_dir) if pin.sdist else None
|
|
174
|
+
),
|
|
175
|
+
wheels=tuple(_wheel_to_package(w, lock_dir=lock_dir) for w in pin.wheels)
|
|
176
|
+
or None,
|
|
138
177
|
)
|
|
139
178
|
if isinstance(pin, LocalPin):
|
|
179
|
+
# PEP 751: omit version for directory sources; it is not
|
|
180
|
+
# deterministic (the source tree may change at install time).
|
|
181
|
+
# The path is recorded relative to the lock file for portability.
|
|
140
182
|
return Package(
|
|
141
183
|
name=canonicalize_name(pin.name),
|
|
142
|
-
version=
|
|
184
|
+
version=None,
|
|
143
185
|
marker=marker,
|
|
144
|
-
directory=PackageDirectory(
|
|
186
|
+
directory=PackageDirectory(
|
|
187
|
+
path=_relativize_path(pin.path, lock_dir),
|
|
188
|
+
editable=pin.editable,
|
|
189
|
+
subdirectory=pin.subdirectory,
|
|
190
|
+
),
|
|
145
191
|
)
|
|
146
192
|
if isinstance(pin, VcsPin):
|
|
193
|
+
# PEP 751: omit version for VCS sources for the same reason.
|
|
147
194
|
return Package(
|
|
148
195
|
name=canonicalize_name(pin.name),
|
|
149
|
-
version=
|
|
196
|
+
version=None,
|
|
150
197
|
marker=marker,
|
|
151
198
|
vcs=PackageVcs(
|
|
152
199
|
type="git",
|
|
153
200
|
url=pin.repo_url,
|
|
154
201
|
commit_id=pin.commit_id,
|
|
155
202
|
subdirectory=pin.subdirectory,
|
|
203
|
+
requested_revision=pin.requested_revision,
|
|
156
204
|
),
|
|
157
205
|
)
|
|
158
206
|
msg = f"unknown pin shape: {pin!r}"
|
|
159
207
|
raise TypeError(msg)
|
|
160
208
|
|
|
161
209
|
|
|
162
|
-
def _wheel_to_package(wheel: WheelArtifact) -> PackageWheel:
|
|
210
|
+
def _wheel_to_package(wheel: WheelArtifact, *, lock_dir: Path) -> PackageWheel:
|
|
211
|
+
"""Convert a wheel artefact to its PEP 751 ``packages.wheels`` entry.
|
|
212
|
+
|
|
213
|
+
A wheel from a local find-links directory carries its on-disk
|
|
214
|
+
``local_path``; it is written as a ``path`` relative to the lock
|
|
215
|
+
file so the lockfile stays portable. A remote wheel records its
|
|
216
|
+
``url`` verbatim.
|
|
217
|
+
"""
|
|
218
|
+
if wheel.local_path is not None:
|
|
219
|
+
return PackageWheel(
|
|
220
|
+
name=wheel.filename,
|
|
221
|
+
path=_relativize_path(wheel.local_path.resolve(), lock_dir),
|
|
222
|
+
size=wheel.size,
|
|
223
|
+
hashes=dict(wheel.hashes),
|
|
224
|
+
upload_time=wheel.upload_time,
|
|
225
|
+
)
|
|
163
226
|
return PackageWheel(
|
|
164
227
|
name=wheel.filename,
|
|
165
228
|
url=wheel.url,
|
|
166
229
|
size=wheel.size,
|
|
167
230
|
hashes=dict(wheel.hashes),
|
|
231
|
+
upload_time=wheel.upload_time,
|
|
168
232
|
)
|
|
169
233
|
|
|
170
234
|
|
|
171
|
-
def _sdist_to_package(sdist: SdistArtifact) -> PackageSdist:
|
|
235
|
+
def _sdist_to_package(sdist: SdistArtifact, *, lock_dir: Path) -> PackageSdist:
|
|
236
|
+
"""Convert an sdist artefact to its PEP 751 ``packages.sdist`` entry.
|
|
237
|
+
|
|
238
|
+
See :func:`_wheel_to_package` for the ``local_path`` handling.
|
|
239
|
+
"""
|
|
240
|
+
if sdist.local_path is not None:
|
|
241
|
+
return PackageSdist(
|
|
242
|
+
name=sdist.filename,
|
|
243
|
+
path=_relativize_path(sdist.local_path.resolve(), lock_dir),
|
|
244
|
+
size=sdist.size,
|
|
245
|
+
hashes=dict(sdist.hashes),
|
|
246
|
+
upload_time=sdist.upload_time,
|
|
247
|
+
)
|
|
172
248
|
return PackageSdist(
|
|
173
249
|
name=sdist.filename,
|
|
174
250
|
url=sdist.url,
|
|
175
251
|
size=sdist.size,
|
|
176
252
|
hashes=dict(sdist.hashes),
|
|
253
|
+
upload_time=sdist.upload_time,
|
|
177
254
|
)
|
|
178
255
|
|
|
179
256
|
|
|
180
|
-
def _build_per_tuple_packages(lock_input: LockInput) -> list[Package]:
|
|
257
|
+
def _build_per_tuple_packages(lock_input: LockInput, lock_dir: Path) -> list[Package]:
|
|
181
258
|
"""Collapse per-tuple pins into Package entries with markers.
|
|
182
259
|
|
|
183
260
|
For each canonical package name:
|
|
@@ -200,12 +277,14 @@ def _build_per_tuple_packages(lock_input: LockInput) -> list[Package]:
|
|
|
200
277
|
groups = _group_pins_by_pin(per_tuple)
|
|
201
278
|
for pins, tuple_labels in groups:
|
|
202
279
|
marker = _build_marker(tuple_labels, lock_input.tuple_markers, total_tuples)
|
|
203
|
-
out.append(
|
|
280
|
+
out.append(
|
|
281
|
+
_pin_to_package(_merge_pins_in_group(pins), marker, lock_dir=lock_dir)
|
|
282
|
+
)
|
|
204
283
|
# Pins only present in lock_input.pins (e.g. tuples agreed via the
|
|
205
284
|
# single-source path) emit unconditionally.
|
|
206
285
|
for canonical_name, pin in lock_input.pins.items():
|
|
207
286
|
if canonical_name not in by_name:
|
|
208
|
-
out.append(_pin_to_package(pin))
|
|
287
|
+
out.append(_pin_to_package(pin, lock_dir=lock_dir))
|
|
209
288
|
return out
|
|
210
289
|
|
|
211
290
|
|
|
@@ -242,8 +321,18 @@ def _pin_discriminator(pin: PinShape) -> tuple:
|
|
|
242
321
|
if isinstance(pin, IndexPin):
|
|
243
322
|
return ("index", pin.version, pin.index)
|
|
244
323
|
if isinstance(pin, LocalPin):
|
|
245
|
-
|
|
324
|
+
# editable and subdirectory change install behaviour, so two
|
|
325
|
+
# otherwise-identical local pins differing only here are distinct.
|
|
326
|
+
return (
|
|
327
|
+
"local",
|
|
328
|
+
pin.version,
|
|
329
|
+
pin.path,
|
|
330
|
+
pin.editable,
|
|
331
|
+
pin.subdirectory or "",
|
|
332
|
+
)
|
|
246
333
|
if isinstance(pin, VcsPin):
|
|
334
|
+
# requested_revision is informational; it does not affect the
|
|
335
|
+
# checkout, so it is intentionally left out of the discriminator.
|
|
247
336
|
return ("vcs", pin.commit_id, pin.repo_url, pin.subdirectory or "")
|
|
248
337
|
msg = f"unknown pin shape: {pin!r}"
|
|
249
338
|
raise TypeError(msg)
|
|
@@ -430,7 +430,11 @@ def add_classified_dep(
|
|
|
430
430
|
base_deps: dict[str, VersionRange],
|
|
431
431
|
extra_deps_map: dict[str, dict[str, VersionRange]],
|
|
432
432
|
) -> None:
|
|
433
|
-
"""Add a classified requirement to the appropriate dep set.
|
|
433
|
+
"""Add a classified requirement to the appropriate dep set.
|
|
434
|
+
|
|
435
|
+
A name appearing on several ``Requires-Dist`` lines is intersected
|
|
436
|
+
into one range.
|
|
437
|
+
"""
|
|
434
438
|
# Late import: ``pypi`` imports this module at module load.
|
|
435
439
|
from ..provider import join_extra
|
|
436
440
|
|
|
@@ -439,12 +443,12 @@ def add_classified_dep(
|
|
|
439
443
|
dep_extras: set[str] = req.extras
|
|
440
444
|
|
|
441
445
|
if not req_extras:
|
|
442
|
-
base_deps[name] = vi
|
|
446
|
+
base_deps[name] = base_deps.get(name, VersionRange.full()) & vi
|
|
443
447
|
for re in dep_extras:
|
|
444
448
|
base_deps[join_extra(name, re)] = VersionRange.full()
|
|
445
449
|
else:
|
|
446
450
|
for extra_name in req_extras:
|
|
447
451
|
edeps = extra_deps_map[extra_name]
|
|
448
|
-
edeps[name] = vi
|
|
452
|
+
edeps[name] = edeps.get(name, VersionRange.full()) & vi
|
|
449
453
|
for re in dep_extras:
|
|
450
454
|
edeps[join_extra(name, re)] = VersionRange.full()
|
nab_python/_provider/sources.py
CHANGED
|
@@ -57,6 +57,8 @@ def materialize_local_source(
|
|
|
57
57
|
or looser; raises :class:`UnsupportedSdistError` otherwise.
|
|
58
58
|
"""
|
|
59
59
|
path = Path(source.path)
|
|
60
|
+
if source.subdirectory:
|
|
61
|
+
path = path / source.subdirectory
|
|
60
62
|
metadata = extract_source_metadata(
|
|
61
63
|
provider,
|
|
62
64
|
path,
|
|
@@ -134,6 +136,7 @@ def seed_synthetic_listing(
|
|
|
134
136
|
else None
|
|
135
137
|
),
|
|
136
138
|
upload_time=None,
|
|
139
|
+
local_path=path,
|
|
137
140
|
)
|
|
138
141
|
version = metadata.version
|
|
139
142
|
provider.metadata_cache[(normalized, version)] = metadata
|
|
@@ -13,7 +13,8 @@ import threading
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
from unittest.mock import MagicMock
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from nab_index.multi_index import IndexConfig
|
|
17
|
+
from nab_python.fetch import DEFAULT_INDEX_NAME, DEFAULT_INDEX_URL, InMemoryIndex
|
|
17
18
|
|
|
18
19
|
if TYPE_CHECKING:
|
|
19
20
|
from collections.abc import Callable, Mapping, Sequence
|
|
@@ -207,7 +208,8 @@ def make_coordinator( # noqa: PLR0913
|
|
|
207
208
|
Call sites that need request side effects beyond what this helper
|
|
208
209
|
wires up (for example ``request_sdist_archive``) can reassign
|
|
209
210
|
``.side_effect`` on the returned mock; the index is exposed at
|
|
210
|
-
``coordinator.index`` for direct manipulation.
|
|
211
|
+
``coordinator.index`` for direct manipulation. ``coordinator.indexes``
|
|
212
|
+
defaults to the single default-PyPI :class:`IndexConfig` list.
|
|
211
213
|
"""
|
|
212
214
|
index = InMemoryIndex()
|
|
213
215
|
failures = fetch_failures if fetch_failures is not None else set()
|
|
@@ -223,6 +225,7 @@ def make_coordinator( # noqa: PLR0913
|
|
|
223
225
|
|
|
224
226
|
coordinator = MagicMock()
|
|
225
227
|
coordinator.index = index
|
|
228
|
+
coordinator.indexes = [IndexConfig(DEFAULT_INDEX_NAME, DEFAULT_INDEX_URL)]
|
|
226
229
|
|
|
227
230
|
resolve_metadata = _make_metadata_resolver(
|
|
228
231
|
metadata_text=metadata_text,
|
|
@@ -9,8 +9,8 @@ from an in-flight pull request, vendored so nab can use the public
|
|
|
9
9
|
- Upstream repository: https://github.com/pypa/packaging
|
|
10
10
|
- Pull request: https://github.com/pypa/packaging/pull/1182
|
|
11
11
|
- Source branch: `notatallshaw/packaging:public-pep440-version-range`
|
|
12
|
-
- Pinned commit: `
|
|
13
|
-
- Snapshot date: 2026-05-
|
|
12
|
+
- Pinned commit: `82799d02ffec2815769d5889062e54686e7c6863`
|
|
13
|
+
- Snapshot date: 2026-05-23
|
|
14
14
|
|
|
15
15
|
The snapshot is the full `src/packaging/` tree at that commit, plus
|
|
16
16
|
`LICENSE`, `LICENSE.APACHE`, and `LICENSE.BSD` from the repository
|