nab-python 0.0.2__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/env.py +1 -1
- nab_python/_build/runner.py +14 -3
- nab_python/_conflict_kind.py +20 -0
- nab_python/_lockfile/builder.py +222 -27
- nab_python/_lockfile/disjointness.py +251 -51
- nab_python/_lockfile/pylock.py +308 -43
- 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 +31 -13
- nab_python/_provider/sources.py +3 -0
- nab_python/_testing/coordinator_fake.py +17 -6
- nab_python/_toml.py +16 -0
- nab_python/_vendor/packaging/PROVENANCE.md +2 -2
- nab_python/_vendor/packaging/_range_utils.py +998 -0
- nab_python/_vendor/packaging/_version_utils.py +90 -0
- nab_python/_vendor/packaging/metadata.py +19 -4
- nab_python/_vendor/packaging/pylock.py +1 -0
- nab_python/_vendor/packaging/ranges.py +1489 -1180
- nab_python/_vendor/packaging/specifiers.py +436 -121
- nab_python/_vendor/packaging/tags.py +8 -6
- nab_python/config.py +647 -9
- nab_python/download.py +39 -9
- nab_python/fetch.py +60 -15
- nab_python/lockfile.py +88 -2
- nab_python/metadata.py +23 -0
- nab_python/provider.py +82 -33
- nab_python/requirements_file.py +129 -15
- nab_python/resolve.py +462 -33
- 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 +266 -34
- 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.2.dist-info → nab_python-0.0.4.dist-info}/METADATA +4 -5
- nab_python-0.0.4.dist-info/RECORD +74 -0
- nab_python-0.0.2.dist-info/RECORD +0 -71
- nab_python-0.0.2.dist-info/licenses/LICENSE +0 -21
- {nab_python-0.0.2.dist-info → nab_python-0.0.4.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/_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,14 +9,18 @@ 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
|
|
16
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
15
17
|
|
|
16
18
|
import tomli
|
|
17
19
|
|
|
18
20
|
from nab_index.client import SdistFile, WheelFile
|
|
19
21
|
|
|
22
|
+
from .._toml import tool_nab_section
|
|
23
|
+
from .._vendor.packaging.pylock import Pylock, PylockValidationError
|
|
20
24
|
from .._vendor.packaging.utils import canonicalize_name
|
|
21
25
|
|
|
22
26
|
if TYPE_CHECKING:
|
|
@@ -38,8 +42,11 @@ if TYPE_CHECKING:
|
|
|
38
42
|
|
|
39
43
|
__all__ = [
|
|
40
44
|
"MissingHashError",
|
|
45
|
+
"MissingSdistError",
|
|
46
|
+
"MissingVcsCommitError",
|
|
41
47
|
"build_lock_input_from_provider",
|
|
42
48
|
"read_lockfile_anchor",
|
|
49
|
+
"read_lockfile_packages",
|
|
43
50
|
]
|
|
44
51
|
|
|
45
52
|
|
|
@@ -68,6 +75,12 @@ class LockInputProvider(Protocol):
|
|
|
68
75
|
may supply a stub without inheriting the full Provider class.
|
|
69
76
|
"""
|
|
70
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
|
+
|
|
71
84
|
@property
|
|
72
85
|
def coordinator(self) -> _LockInputCoordinator:
|
|
73
86
|
"""Coordinator used to look up the index that served a listing."""
|
|
@@ -108,6 +121,30 @@ class MissingHashError(ValueError):
|
|
|
108
121
|
"""
|
|
109
122
|
|
|
110
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
|
+
|
|
111
148
|
def read_lockfile_anchor(path: Path) -> datetime | None:
|
|
112
149
|
"""Return the ``[tool.nab].created-at`` timestamp from ``path`` if any.
|
|
113
150
|
|
|
@@ -128,19 +165,60 @@ def read_lockfile_anchor(path: Path) -> datetime | None:
|
|
|
128
165
|
data = tomli.load(f)
|
|
129
166
|
except (OSError, tomli.TOMLDecodeError):
|
|
130
167
|
return None
|
|
131
|
-
|
|
168
|
+
nab = tool_nab_section(data)
|
|
169
|
+
raw = nab.get("created-at") if isinstance(nab, dict) else None
|
|
132
170
|
if isinstance(raw, datetime):
|
|
133
171
|
return raw if raw.tzinfo else raw.replace(tzinfo=timezone.utc)
|
|
134
172
|
if isinstance(raw, str):
|
|
173
|
+
# Python 3.10's fromisoformat rejects a trailing 'Z'; 3.11+ accept it.
|
|
174
|
+
iso = raw[:-1] + "+00:00" if raw.endswith("Z") else raw
|
|
135
175
|
try:
|
|
136
|
-
dt = datetime.fromisoformat(
|
|
176
|
+
dt = datetime.fromisoformat(iso)
|
|
137
177
|
except ValueError:
|
|
138
178
|
return None
|
|
139
179
|
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
140
180
|
return None
|
|
141
181
|
|
|
142
182
|
|
|
143
|
-
def
|
|
183
|
+
def read_lockfile_packages(path: Path) -> dict[str, Version] | None:
|
|
184
|
+
"""Return the ``name -> version`` map from a prior pylock at ``path``.
|
|
185
|
+
|
|
186
|
+
Used by ``nab lock`` to diff a re-lock against the previous result.
|
|
187
|
+
Packages without a recorded version (direct-reference entries that
|
|
188
|
+
omit it) are skipped.
|
|
189
|
+
|
|
190
|
+
Returns ``None`` when ``path`` does not exist, is not valid TOML,
|
|
191
|
+
or is not a spec-compliant PEP 751 lockfile; the caller falls back
|
|
192
|
+
to a no-diff summary line.
|
|
193
|
+
"""
|
|
194
|
+
if not path.is_file():
|
|
195
|
+
return None
|
|
196
|
+
try:
|
|
197
|
+
with path.open("rb") as f:
|
|
198
|
+
data = tomli.load(f)
|
|
199
|
+
pylock = Pylock.from_dict(data)
|
|
200
|
+
except (OSError, tomli.TOMLDecodeError, PylockValidationError):
|
|
201
|
+
return None
|
|
202
|
+
return {
|
|
203
|
+
str(pkg.name): pkg.version for pkg in pylock.packages if pkg.version is not None
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _strip_userinfo(url: str) -> str:
|
|
208
|
+
"""Return ``url`` with any ``user:password@`` userinfo removed.
|
|
209
|
+
|
|
210
|
+
Lockfiles are committed to version control, so an index or VCS
|
|
211
|
+
URL carrying embedded credentials must not be written verbatim.
|
|
212
|
+
Only the userinfo is dropped: host case and port are preserved
|
|
213
|
+
and a ``git+`` scheme prefix is left intact. A no-op for URLs
|
|
214
|
+
without credentials.
|
|
215
|
+
"""
|
|
216
|
+
parts = urlsplit(url)
|
|
217
|
+
netloc = parts.netloc.rpartition("@")[2]
|
|
218
|
+
return urlunsplit(parts._replace(netloc=netloc))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_lock_input_from_provider( # noqa: PLR0913 - each flag maps to a distinct lockfile field
|
|
144
222
|
provider: LockInputProvider,
|
|
145
223
|
pins: Mapping[str, Version],
|
|
146
224
|
*,
|
|
@@ -150,6 +228,7 @@ def build_lock_input_from_provider(
|
|
|
150
228
|
default_groups: Sequence[str] = (),
|
|
151
229
|
created_by: str = "nab",
|
|
152
230
|
indexes: Sequence[IndexConfig] = (),
|
|
231
|
+
resolved_keys: Iterable[str] = (),
|
|
153
232
|
) -> LockInput:
|
|
154
233
|
"""Build a :class:`LockInput` from a finished resolve.
|
|
155
234
|
|
|
@@ -162,6 +241,10 @@ def build_lock_input_from_provider(
|
|
|
162
241
|
were folded into this resolve; ``default_groups`` is the subset
|
|
163
242
|
that a default install (no ``--group`` flag) should apply.
|
|
164
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
|
+
|
|
165
248
|
All wheels and the sdist for each pinned version are recorded so
|
|
166
249
|
the lockfile is portable across architectures of the same Python.
|
|
167
250
|
"""
|
|
@@ -176,6 +259,8 @@ def build_lock_input_from_provider(
|
|
|
176
259
|
name=canonical,
|
|
177
260
|
version=str(version),
|
|
178
261
|
path=str(Path(local_source.path).resolve()),
|
|
262
|
+
editable=local_source.editable,
|
|
263
|
+
subdirectory=local_source.subdirectory,
|
|
179
264
|
)
|
|
180
265
|
continue
|
|
181
266
|
vcs_source = provider.vcs_source_for(canonical)
|
|
@@ -197,9 +282,53 @@ def build_lock_input_from_provider(
|
|
|
197
282
|
extras=tuple(extras),
|
|
198
283
|
dependency_groups=tuple(dependency_groups),
|
|
199
284
|
default_groups=tuple(default_groups),
|
|
285
|
+
dependencies=_forward_dependency_graph(provider, pins, resolved_keys),
|
|
200
286
|
)
|
|
201
287
|
|
|
202
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
|
+
|
|
203
332
|
def _index_pin_from_listing(
|
|
204
333
|
provider: LockInputProvider,
|
|
205
334
|
canonical: str,
|
|
@@ -212,8 +341,8 @@ def _index_pin_from_listing(
|
|
|
212
341
|
served the package's listing during the resolve, looked up from
|
|
213
342
|
the coordinator's :class:`InMemoryIndex` (which records the
|
|
214
343
|
serving index by name) and resolved against ``indexes`` for the
|
|
215
|
-
URL.
|
|
216
|
-
|
|
344
|
+
URL. A pinned package's serving index is always recorded and is
|
|
345
|
+
one of ``indexes``, so the URL is always known.
|
|
217
346
|
|
|
218
347
|
Under :attr:`~nab_python.provider.DistPolicy.SDIST_INSTALL` the
|
|
219
348
|
package's wheels stayed in ``versions_cache`` as a possible
|
|
@@ -227,6 +356,13 @@ def _index_pin_from_listing(
|
|
|
227
356
|
files = list(provider.dist_files_for(canonical, version))
|
|
228
357
|
if provider.effective_dist_policy(canonical) is DistPolicy.SDIST_INSTALL:
|
|
229
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)
|
|
230
366
|
|
|
231
367
|
wheels = tuple(
|
|
232
368
|
_build_artifact(canonical, f, WheelArtifact)
|
|
@@ -242,17 +378,22 @@ def _index_pin_from_listing(
|
|
|
242
378
|
requires_python = _common_requires_python(files)
|
|
243
379
|
serving_name = provider.coordinator.index.get_listing_index(canonical)
|
|
244
380
|
by_name = {ix.name: ix.url for ix in indexes}
|
|
245
|
-
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|
|
251
392
|
index_url = DEFAULT_INDEX_URL
|
|
252
393
|
return IndexPin(
|
|
253
394
|
name=canonical,
|
|
254
395
|
version=str(version),
|
|
255
|
-
index=index_url,
|
|
396
|
+
index=_strip_userinfo(index_url),
|
|
256
397
|
sdist=sdist,
|
|
257
398
|
wheels=wheels,
|
|
258
399
|
requires_python=requires_python,
|
|
@@ -282,9 +423,31 @@ def _build_artifact(
|
|
|
282
423
|
url=source.url,
|
|
283
424
|
hashes=hashes,
|
|
284
425
|
size=source.size,
|
|
426
|
+
upload_time=_parse_upload_time(source.upload_time),
|
|
427
|
+
local_path=source.local_path,
|
|
285
428
|
)
|
|
286
429
|
|
|
287
430
|
|
|
431
|
+
def _parse_upload_time(raw: str | None) -> datetime | None:
|
|
432
|
+
"""Parse an index ``upload-time`` string to a UTC ``datetime``.
|
|
433
|
+
|
|
434
|
+
Accepts the RFC 3339 form the Simple/JSON API serves (``Z`` or an
|
|
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.
|
|
439
|
+
"""
|
|
440
|
+
if raw is None:
|
|
441
|
+
return None
|
|
442
|
+
try:
|
|
443
|
+
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
444
|
+
except ValueError:
|
|
445
|
+
return None
|
|
446
|
+
if parsed.tzinfo is None:
|
|
447
|
+
return None
|
|
448
|
+
return parsed.astimezone(timezone.utc)
|
|
449
|
+
|
|
450
|
+
|
|
288
451
|
def _filter_acceptable_hashes(
|
|
289
452
|
canonical: str, filename: str, hashes: tuple[tuple[str, str], ...]
|
|
290
453
|
) -> tuple[tuple[str, str], ...]:
|
|
@@ -332,26 +495,58 @@ def _vcs_pin_from_source(
|
|
|
332
495
|
) -> VcsPin:
|
|
333
496
|
"""Build a :class:`VcsPin` from a :class:`VcsSource`.
|
|
334
497
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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``.
|
|
504
|
+
|
|
505
|
+
``requested_revision`` is the URL's ``@<ref>``, kept only when it
|
|
506
|
+
is a named ref that differs from ``commit_id`` (i.e. the user did
|
|
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.
|
|
339
517
|
"""
|
|
340
|
-
from nab_index.vcs import
|
|
518
|
+
from nab_index.vcs import VcsRequest
|
|
341
519
|
|
|
342
520
|
from ..lockfile import VcsPin
|
|
343
521
|
|
|
344
|
-
if resolved_sha is
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
|
|
352
543
|
return VcsPin(
|
|
353
544
|
name=canonical,
|
|
354
545
|
version=str(version),
|
|
355
|
-
repo_url=
|
|
356
|
-
|
|
546
|
+
repo_url=repo_url,
|
|
547
|
+
bare_repo_url=bare_repo_url,
|
|
548
|
+
commit_id=resolved_sha,
|
|
549
|
+
subdirectory=parsed.subdirectory or None,
|
|
550
|
+
requested_revision=requested_revision,
|
|
551
|
+
vcs_type=parsed.scheme,
|
|
357
552
|
)
|