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,174 @@
|
|
|
1
|
+
"""Priority computation for :class:`nab_python.provider.Provider`.
|
|
2
|
+
|
|
3
|
+
Owns the tier/matching/culprit logic that backs ``prioritize``.
|
|
4
|
+
Affected packages with high conflict counts get tier 0 (decide
|
|
5
|
+
first inside a conflict cluster); runaway top culprits get tier 2
|
|
6
|
+
(uv's deprioritise-on-conflict); everything else gets tier 1.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from nab_resolver.types import RangeProtocol
|
|
17
|
+
|
|
18
|
+
from .._vendor.packaging.version import Version
|
|
19
|
+
from ..provider import Provider
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CONFLICT_THRESHOLD = 5
|
|
23
|
+
|
|
24
|
+
# Demotion requires a runaway gap to the second-highest culprit, so
|
|
25
|
+
# co-dominant culprits keep standard ordering.
|
|
26
|
+
CULPRIT_DEMOTE_THRESHOLD = 5
|
|
27
|
+
|
|
28
|
+
# Lower number = higher priority.
|
|
29
|
+
TIER_AFFECTED = 0
|
|
30
|
+
TIER_NORMAL = 1
|
|
31
|
+
TIER_CULPRIT = 2
|
|
32
|
+
|
|
33
|
+
# Matching count used while a listing is in flight, so not-yet-fetched
|
|
34
|
+
# packages sort behind ready ones.
|
|
35
|
+
_NO_LISTING_PRIOR = 1000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def compute_tier(
|
|
39
|
+
normalized: str,
|
|
40
|
+
affected_count: int,
|
|
41
|
+
culprit_count: int,
|
|
42
|
+
culprit_counts: Mapping[str, int] | None,
|
|
43
|
+
*,
|
|
44
|
+
force_backtracked: bool = False,
|
|
45
|
+
) -> int:
|
|
46
|
+
"""Decide the priority tier from conflict and culprit counts.
|
|
47
|
+
|
|
48
|
+
``force_backtracked`` short-circuits the gap rule: the look-ahead
|
|
49
|
+
abort is a precise enough culprit signal on its own.
|
|
50
|
+
"""
|
|
51
|
+
if affected_count >= CONFLICT_THRESHOLD:
|
|
52
|
+
return TIER_AFFECTED
|
|
53
|
+
if force_backtracked:
|
|
54
|
+
return TIER_CULPRIT
|
|
55
|
+
if is_dominant_culprit(normalized, culprit_count, culprit_counts):
|
|
56
|
+
return TIER_CULPRIT
|
|
57
|
+
return TIER_NORMAL
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def compute_matching(
|
|
61
|
+
provider: Provider,
|
|
62
|
+
normalized: str,
|
|
63
|
+
version_range: RangeProtocol[Version],
|
|
64
|
+
) -> int:
|
|
65
|
+
"""Return the count of cached versions of ``normalized`` in ``version_range``.
|
|
66
|
+
|
|
67
|
+
Also fires speculative metadata prefetch when this is the first time we
|
|
68
|
+
notice the listing has arrived in the coordinator index. Returns
|
|
69
|
+
:data:`_NO_LISTING_PRIOR` while the listing is still in flight.
|
|
70
|
+
"""
|
|
71
|
+
per_pkg = provider.matching_cache.get(normalized)
|
|
72
|
+
if per_pkg is not None:
|
|
73
|
+
cached = per_pkg.get(version_range)
|
|
74
|
+
if cached is not None:
|
|
75
|
+
return cached
|
|
76
|
+
|
|
77
|
+
# Local/VCS sources short-circuit the listing path; their synthetic
|
|
78
|
+
# listing is materialised lazily by fetch_versions.
|
|
79
|
+
has_local_source = (
|
|
80
|
+
normalized in provider.local_sources or normalized in provider.vcs_sources
|
|
81
|
+
)
|
|
82
|
+
if normalized not in provider.versions_cache and not has_local_source:
|
|
83
|
+
files = provider.coordinator.index.get_listing(normalized)
|
|
84
|
+
if files is not None:
|
|
85
|
+
versions = provider.filter_distributions(normalized, files)
|
|
86
|
+
provider.versions_cache[normalized] = versions
|
|
87
|
+
provider.stats.listings_fetched += 1
|
|
88
|
+
provider.speculative_prefetch(normalized, versions)
|
|
89
|
+
|
|
90
|
+
if normalized in provider.versions_cache:
|
|
91
|
+
versions = provider.versions_cache[normalized]
|
|
92
|
+
matching = sum(1 for v, _ in versions if v in version_range)
|
|
93
|
+
elif has_local_source:
|
|
94
|
+
matching = 1
|
|
95
|
+
else:
|
|
96
|
+
matching = _NO_LISTING_PRIOR
|
|
97
|
+
|
|
98
|
+
if per_pkg is None:
|
|
99
|
+
per_pkg = provider.matching_cache[normalized] = {}
|
|
100
|
+
per_pkg[version_range] = matching
|
|
101
|
+
return matching
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_dominant_culprit(
|
|
105
|
+
package: str,
|
|
106
|
+
package_count: int,
|
|
107
|
+
culprit_counts: Mapping[str, int] | None,
|
|
108
|
+
) -> bool:
|
|
109
|
+
"""Return True when ``package`` is the runaway top culprit.
|
|
110
|
+
|
|
111
|
+
Demote only when the gap to the next culprit is >= CULPRIT_DEMOTE_THRESHOLD;
|
|
112
|
+
co-dominant culprits stay within ~1 of each other so the standard ordering
|
|
113
|
+
wins.
|
|
114
|
+
"""
|
|
115
|
+
if culprit_counts is None or package_count < CULPRIT_DEMOTE_THRESHOLD:
|
|
116
|
+
return False
|
|
117
|
+
second_highest = max(
|
|
118
|
+
(count for other, count in culprit_counts.items() if other != package),
|
|
119
|
+
default=0,
|
|
120
|
+
)
|
|
121
|
+
return package_count - second_highest >= CULPRIT_DEMOTE_THRESHOLD
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def prioritize(
|
|
125
|
+
provider: Provider,
|
|
126
|
+
package: str,
|
|
127
|
+
version_range: RangeProtocol[Version],
|
|
128
|
+
conflict_counts: Mapping[str, int],
|
|
129
|
+
culprit_counts: Mapping[str, int] | None = None,
|
|
130
|
+
) -> tuple[int, int, bool]:
|
|
131
|
+
"""Prioritize packages for resolution order.
|
|
132
|
+
|
|
133
|
+
Returns ``(tier, matching_count, is_base)``. Extras proxies sort before
|
|
134
|
+
their base at equal tier so they pin the base version directly (avoids
|
|
135
|
+
the backtrack storm when the base is decided before the extras proxy).
|
|
136
|
+
|
|
137
|
+
Never blocks on I/O.
|
|
138
|
+
"""
|
|
139
|
+
provider.stats.prioritize_calls += 1
|
|
140
|
+
_, extra, normalized = provider.split_and_normalize(package)
|
|
141
|
+
affected_count = conflict_counts.get(normalized, 0)
|
|
142
|
+
culprit_count = (
|
|
143
|
+
culprit_counts.get(normalized, 0) if culprit_counts is not None else 0
|
|
144
|
+
)
|
|
145
|
+
force_backtracked = provider.force_backtrack_count(normalized) > 0
|
|
146
|
+
|
|
147
|
+
# Fast path: when culprit_count is below the demote threshold AND the
|
|
148
|
+
# package was not force-backtracked, the tier depends only on
|
|
149
|
+
# affected_count and is safe to cache by Range identity.
|
|
150
|
+
cacheable = culprit_count < CULPRIT_DEMOTE_THRESHOLD and not force_backtracked
|
|
151
|
+
if cacheable:
|
|
152
|
+
cached = provider.priority_cache.get(package)
|
|
153
|
+
if (
|
|
154
|
+
cached is not None
|
|
155
|
+
and cached[0] is version_range
|
|
156
|
+
and cached[1] == affected_count
|
|
157
|
+
):
|
|
158
|
+
return cached[2]
|
|
159
|
+
|
|
160
|
+
tier = compute_tier(
|
|
161
|
+
normalized,
|
|
162
|
+
affected_count,
|
|
163
|
+
culprit_count,
|
|
164
|
+
culprit_counts,
|
|
165
|
+
force_backtracked=force_backtracked,
|
|
166
|
+
)
|
|
167
|
+
matching = compute_matching(provider, normalized, version_range)
|
|
168
|
+
priority = (tier, matching, extra is None)
|
|
169
|
+
|
|
170
|
+
# Don't cache the in-flight placeholder; compute_matching's listing-arrival
|
|
171
|
+
# side effect (speculative prefetch) lives in the cache-miss branch.
|
|
172
|
+
if cacheable and normalized in provider.versions_cache:
|
|
173
|
+
provider.priority_cache[package] = (version_range, affected_count, priority)
|
|
174
|
+
return priority
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Local-source and VCS-source materialisation for the provider.
|
|
2
|
+
|
|
3
|
+
A ``LocalSource`` becomes the only candidate for a package: PyPI is
|
|
4
|
+
not consulted. A ``VcsSource`` clones the repo and reuses the
|
|
5
|
+
``LocalSource`` extraction path. Both produce a single synthetic
|
|
6
|
+
``SdistFile`` whose version is read from ``[project].version``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from nab_index.client import SdistFile
|
|
15
|
+
from nab_index.vcs import VcsCloneError, VcsRequest
|
|
16
|
+
|
|
17
|
+
from .._vendor.packaging.utils import canonicalize_name
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .._vendor.packaging.version import Version
|
|
21
|
+
from ..metadata import WheelMetadata
|
|
22
|
+
from ..provider import LocalSource, Provider, VcsSource
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def index_local_sources(
|
|
26
|
+
provider: Provider, # noqa: ARG001 (signature parity with index_vcs_sources)
|
|
27
|
+
sources: list[LocalSource],
|
|
28
|
+
) -> dict[str, LocalSource]:
|
|
29
|
+
"""Validate ``LocalSource`` entries and return a canonical-name map.
|
|
30
|
+
|
|
31
|
+
Admitted at every :class:`~nab_python.provider.BuildPolicy` level; the
|
|
32
|
+
policy only governs whether the backend may run when the static
|
|
33
|
+
pyproject read returns nothing usable (see
|
|
34
|
+
:func:`extract_source_metadata`).
|
|
35
|
+
"""
|
|
36
|
+
if not sources:
|
|
37
|
+
return {}
|
|
38
|
+
out: dict[str, LocalSource] = {}
|
|
39
|
+
for src in sources:
|
|
40
|
+
canonical = canonicalize_name(src.name)
|
|
41
|
+
if canonical in out:
|
|
42
|
+
msg = f"duplicate local source for {src.name!r}"
|
|
43
|
+
raise ValueError(msg)
|
|
44
|
+
out[canonical] = src
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def materialize_local_source(
|
|
49
|
+
provider: Provider,
|
|
50
|
+
normalized: str,
|
|
51
|
+
source: LocalSource,
|
|
52
|
+
) -> list[tuple[Version, SdistFile]]:
|
|
53
|
+
"""Read metadata from ``source`` and seed caches with one synthetic version.
|
|
54
|
+
|
|
55
|
+
Static path: ``extract_static_metadata`` reads ``pyproject.toml``
|
|
56
|
+
directly. Backend path: requires :attr:`BuildPolicy.BUILD_LOCAL`
|
|
57
|
+
or looser; raises :class:`UnsupportedSdistError` otherwise.
|
|
58
|
+
"""
|
|
59
|
+
path = Path(source.path)
|
|
60
|
+
metadata = extract_source_metadata(
|
|
61
|
+
provider,
|
|
62
|
+
path,
|
|
63
|
+
descriptor=f"local source {source.name!r}",
|
|
64
|
+
package=canonicalize_name(source.name),
|
|
65
|
+
kind="local",
|
|
66
|
+
)
|
|
67
|
+
return seed_synthetic_listing(provider, normalized, path, metadata)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_source_metadata(
|
|
71
|
+
provider: Provider,
|
|
72
|
+
path: Path,
|
|
73
|
+
*,
|
|
74
|
+
descriptor: str,
|
|
75
|
+
package: str,
|
|
76
|
+
kind: str,
|
|
77
|
+
) -> WheelMetadata:
|
|
78
|
+
"""Read metadata from a directory; gates the backend path on policy.
|
|
79
|
+
|
|
80
|
+
``kind`` is ``"local"`` for :class:`LocalSource` directories
|
|
81
|
+
(admitted at :attr:`BuildPolicy.BUILD_LOCAL` and above) or
|
|
82
|
+
``"vcs"`` for :class:`VcsSource` clones (admitted only at
|
|
83
|
+
:attr:`BuildPolicy.BUILD_REMOTE`).
|
|
84
|
+
"""
|
|
85
|
+
# Module-level attribute access lets tests patch
|
|
86
|
+
# ``nab_python.build_backend.extract_metadata`` from the source.
|
|
87
|
+
from .. import build_backend
|
|
88
|
+
from ..build_backend import BuildBackendError, extract_static_metadata
|
|
89
|
+
from ..provider import BuildPolicy, UnsupportedSdistError
|
|
90
|
+
|
|
91
|
+
metadata = extract_static_metadata(path)
|
|
92
|
+
if metadata is not None:
|
|
93
|
+
return metadata
|
|
94
|
+
effective = provider.effective_build_policy(package)
|
|
95
|
+
if kind == "local":
|
|
96
|
+
allowed = {BuildPolicy.BUILD_LOCAL, BuildPolicy.BUILD_REMOTE}
|
|
97
|
+
minimum = BuildPolicy.BUILD_LOCAL
|
|
98
|
+
else:
|
|
99
|
+
allowed = {BuildPolicy.BUILD_REMOTE}
|
|
100
|
+
minimum = BuildPolicy.BUILD_REMOTE
|
|
101
|
+
if effective not in allowed:
|
|
102
|
+
provider.stats.excluded_by_build_policy += 1
|
|
103
|
+
msg = (
|
|
104
|
+
f"{descriptor} at {path} has dynamic metadata; building requires"
|
|
105
|
+
f" BuildPolicy.{minimum.name} but the effective policy is"
|
|
106
|
+
f" {effective.value}"
|
|
107
|
+
)
|
|
108
|
+
raise UnsupportedSdistError(msg)
|
|
109
|
+
try:
|
|
110
|
+
return build_backend.extract_metadata(
|
|
111
|
+
path,
|
|
112
|
+
config=provider.build_config,
|
|
113
|
+
python_version=provider.python_version,
|
|
114
|
+
)
|
|
115
|
+
except BuildBackendError as exc:
|
|
116
|
+
msg = f"{descriptor}: {exc}"
|
|
117
|
+
raise UnsupportedSdistError(msg) from exc
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def seed_synthetic_listing(
|
|
121
|
+
provider: Provider,
|
|
122
|
+
normalized: str,
|
|
123
|
+
path: Path,
|
|
124
|
+
metadata: WheelMetadata,
|
|
125
|
+
) -> list[tuple[Version, SdistFile]]:
|
|
126
|
+
"""Produce a one-version listing for a materialised source."""
|
|
127
|
+
synthetic_file = SdistFile(
|
|
128
|
+
filename=f"{normalized}-{metadata.version}.tar.gz",
|
|
129
|
+
url=path.as_uri(),
|
|
130
|
+
version=str(metadata.version),
|
|
131
|
+
requires_python=(
|
|
132
|
+
str(metadata.requires_python)
|
|
133
|
+
if metadata.requires_python is not None
|
|
134
|
+
else None
|
|
135
|
+
),
|
|
136
|
+
upload_time=None,
|
|
137
|
+
)
|
|
138
|
+
version = metadata.version
|
|
139
|
+
provider.metadata_cache[(normalized, version)] = metadata
|
|
140
|
+
return [(version, synthetic_file)]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def index_vcs_sources(
|
|
144
|
+
provider: Provider,
|
|
145
|
+
sources: list[VcsSource],
|
|
146
|
+
) -> dict[str, VcsSource]:
|
|
147
|
+
"""Validate VCS sources and return a canonical-name map.
|
|
148
|
+
|
|
149
|
+
Admitted at every :class:`~nab_python.provider.BuildPolicy` level; the
|
|
150
|
+
policy only governs whether the backend may run on the clone (see
|
|
151
|
+
:func:`extract_source_metadata`). ``VcsPolicy.BLOCK`` still refuses
|
|
152
|
+
any declaration up-front because that is an independent decision
|
|
153
|
+
about whether VCS fetching is permitted at all.
|
|
154
|
+
"""
|
|
155
|
+
# Late import: ``pypi`` imports this module at module load.
|
|
156
|
+
from ..provider import VcsPolicy
|
|
157
|
+
|
|
158
|
+
if not sources:
|
|
159
|
+
return {}
|
|
160
|
+
|
|
161
|
+
if provider.vcs_config.policy is VcsPolicy.BLOCK:
|
|
162
|
+
msg = (
|
|
163
|
+
"vcs_sources require VcsPolicy.ALLOW; current policy is"
|
|
164
|
+
f" {provider.vcs_config.policy.value}. Set vcs_config to"
|
|
165
|
+
" a permissive VcsConfig before declaring sources."
|
|
166
|
+
)
|
|
167
|
+
raise ValueError(msg)
|
|
168
|
+
|
|
169
|
+
out: dict[str, VcsSource] = {}
|
|
170
|
+
for src in sources:
|
|
171
|
+
canonical = canonicalize_name(src.name)
|
|
172
|
+
if canonical in out or canonical in provider.local_sources:
|
|
173
|
+
msg = f"duplicate source declared for {src.name!r}"
|
|
174
|
+
raise ValueError(msg)
|
|
175
|
+
out[canonical] = src
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def materialize_vcs_source(
|
|
180
|
+
provider: Provider,
|
|
181
|
+
normalized: str,
|
|
182
|
+
source: VcsSource,
|
|
183
|
+
) -> list[tuple[Version, SdistFile]]:
|
|
184
|
+
"""Clone ``source`` and materialise it via the same path as a LocalSource."""
|
|
185
|
+
# Module-level attribute access lets tests patch
|
|
186
|
+
# ``nab_index.vcs.prepare_clone`` from the source.
|
|
187
|
+
from nab_index import vcs as _vcs
|
|
188
|
+
|
|
189
|
+
from ..provider import UnsupportedSdistError
|
|
190
|
+
|
|
191
|
+
if provider.vcs_cache_dir is None:
|
|
192
|
+
msg = (
|
|
193
|
+
f"vcs source {source.name!r} declared but no"
|
|
194
|
+
f" vcs_cache_dir was supplied to Provider"
|
|
195
|
+
)
|
|
196
|
+
raise UnsupportedSdistError(msg)
|
|
197
|
+
try:
|
|
198
|
+
request = VcsRequest.parse(source.url)
|
|
199
|
+
clone = _vcs.prepare_clone(
|
|
200
|
+
provider.vcs_cache_dir,
|
|
201
|
+
request,
|
|
202
|
+
require_pin=provider.vcs_config.require_pin,
|
|
203
|
+
)
|
|
204
|
+
except VcsCloneError as exc:
|
|
205
|
+
msg = f"vcs source {source.name!r}: {exc}"
|
|
206
|
+
raise UnsupportedSdistError(msg) from exc
|
|
207
|
+
path = clone.path / clone.subdirectory if clone.subdirectory else clone.path
|
|
208
|
+
metadata = extract_source_metadata(
|
|
209
|
+
provider,
|
|
210
|
+
path,
|
|
211
|
+
descriptor=f"vcs source {source.name!r}",
|
|
212
|
+
package=canonicalize_name(source.name),
|
|
213
|
+
kind="vcs",
|
|
214
|
+
)
|
|
215
|
+
return seed_synthetic_listing(provider, normalized, path, metadata)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal test helpers for nab-python."""
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Shared mock-coordinator builder for the nab-python test suite.
|
|
2
|
+
|
|
3
|
+
A ``FetchCoordinator``-shaped :class:`unittest.mock.MagicMock` wrapped
|
|
4
|
+
around a real :class:`~nab_python.fetch.InMemoryIndex`. The mock's
|
|
5
|
+
request methods write to the index and return an already-set
|
|
6
|
+
:class:`threading.Event`, so the synchronous provider code under test
|
|
7
|
+
sees fetches resolve immediately.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
from unittest.mock import MagicMock
|
|
15
|
+
|
|
16
|
+
from nab_python.fetch import InMemoryIndex
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
20
|
+
|
|
21
|
+
from nab_index.client import SdistFile, WheelFile
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_MINIMAL_METADATA = "Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _done_event() -> threading.Event:
|
|
28
|
+
"""Return an already-set :class:`threading.Event`."""
|
|
29
|
+
ev = threading.Event()
|
|
30
|
+
ev.set()
|
|
31
|
+
return ev
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _pre_populate_index(
|
|
35
|
+
index: InMemoryIndex,
|
|
36
|
+
listings_map: Mapping[str, Sequence[WheelFile | SdistFile]],
|
|
37
|
+
*,
|
|
38
|
+
baseline_metadata: Mapping[str, str] | None,
|
|
39
|
+
per_wheel_metadata: Mapping[str, str] | None,
|
|
40
|
+
sdist_pyproject: Mapping[str, str] | None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Load listings and pre-store validator-visible slots into ``index``."""
|
|
43
|
+
for pkg_name, pkg_wheels in listings_map.items():
|
|
44
|
+
index.store_listing(pkg_name, pkg_wheels)
|
|
45
|
+
if baseline_metadata is not None and pkg_name in baseline_metadata:
|
|
46
|
+
for w in pkg_wheels:
|
|
47
|
+
index.store_metadata(pkg_name, w.version, baseline_metadata[pkg_name])
|
|
48
|
+
break
|
|
49
|
+
if per_wheel_metadata is not None:
|
|
50
|
+
for w in pkg_wheels:
|
|
51
|
+
if w.filename in per_wheel_metadata:
|
|
52
|
+
index.store_metadata(
|
|
53
|
+
pkg_name,
|
|
54
|
+
f"{w.version}#{w.filename}",
|
|
55
|
+
per_wheel_metadata[w.filename],
|
|
56
|
+
)
|
|
57
|
+
if sdist_pyproject is not None and pkg_name in sdist_pyproject:
|
|
58
|
+
for w in pkg_wheels:
|
|
59
|
+
index.store_sdist_pyproject(
|
|
60
|
+
pkg_name, w.version, sdist_pyproject[pkg_name]
|
|
61
|
+
)
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_listings(
|
|
66
|
+
wheels: Sequence[WheelFile | SdistFile] | None,
|
|
67
|
+
package: str,
|
|
68
|
+
listings: Mapping[str, Sequence[WheelFile | SdistFile]] | None,
|
|
69
|
+
) -> Mapping[str, Sequence[WheelFile | SdistFile]]:
|
|
70
|
+
"""Pick the listings map: explicit ``listings`` wins over ``wheels``."""
|
|
71
|
+
if listings is not None:
|
|
72
|
+
return listings
|
|
73
|
+
if wheels is not None:
|
|
74
|
+
return {package: wheels}
|
|
75
|
+
return {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _make_metadata_resolver(
|
|
79
|
+
*,
|
|
80
|
+
metadata_text: str | None,
|
|
81
|
+
metadata_by_version: Mapping[str, str | None] | None,
|
|
82
|
+
auto_metadata: bool,
|
|
83
|
+
) -> Callable[[str, str], str | None]:
|
|
84
|
+
"""Return a callable that picks metadata text for ``(pkg, version)``."""
|
|
85
|
+
|
|
86
|
+
def _resolve(pkg: str, ver: str) -> str | None:
|
|
87
|
+
if metadata_by_version is not None:
|
|
88
|
+
return metadata_by_version.get(ver)
|
|
89
|
+
if metadata_text is not None:
|
|
90
|
+
return metadata_text
|
|
91
|
+
if auto_metadata:
|
|
92
|
+
return _MINIMAL_METADATA.format(name=pkg, version=ver)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
return _resolve
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _wire_metadata_side_effects(
|
|
99
|
+
coordinator: MagicMock,
|
|
100
|
+
index: InMemoryIndex,
|
|
101
|
+
resolve_metadata: Callable[[str, str], str | None],
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Attach ``request_listing``/``request_metadata``/batch side effects."""
|
|
104
|
+
|
|
105
|
+
def _request_listing(_pkg: str) -> threading.Event:
|
|
106
|
+
return _done_event()
|
|
107
|
+
|
|
108
|
+
def _request_metadata(pkg: str, ver: str, _url: str) -> threading.Event:
|
|
109
|
+
text = resolve_metadata(pkg, ver)
|
|
110
|
+
if text is not None:
|
|
111
|
+
index.store_metadata(pkg, ver, text)
|
|
112
|
+
return _done_event()
|
|
113
|
+
|
|
114
|
+
def _request_metadata_batch(
|
|
115
|
+
items: list[tuple[str, str, str]],
|
|
116
|
+
) -> list[tuple[str, str, threading.Event]]:
|
|
117
|
+
results: list[tuple[str, str, threading.Event]] = []
|
|
118
|
+
for pkg, ver, _url in items:
|
|
119
|
+
text = resolve_metadata(pkg, ver)
|
|
120
|
+
if text is not None:
|
|
121
|
+
index.store_metadata(pkg, ver, text)
|
|
122
|
+
results.append((pkg, ver, _done_event()))
|
|
123
|
+
return results
|
|
124
|
+
|
|
125
|
+
coordinator.request_listing.side_effect = _request_listing
|
|
126
|
+
coordinator.request_metadata.side_effect = _request_metadata
|
|
127
|
+
coordinator.request_metadata_batch.side_effect = _request_metadata_batch
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _wire_sdist_side_effects(
|
|
131
|
+
coordinator: MagicMock,
|
|
132
|
+
index: InMemoryIndex,
|
|
133
|
+
*,
|
|
134
|
+
sdist_pkg_info: str | None,
|
|
135
|
+
sdist_pyproject_toml: str | None,
|
|
136
|
+
failures: set[str],
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Attach ``request_sdist`` and ``request_wheel_metadata`` side effects."""
|
|
139
|
+
|
|
140
|
+
def _request_sdist(pkg: str, ver: str, _url: str) -> threading.Event:
|
|
141
|
+
# ``store_sdist_metadata`` is always called; passing ``None``
|
|
142
|
+
# poisons the cache slot, matching the original test_provider
|
|
143
|
+
# helper's contract for sdist-fetch failures.
|
|
144
|
+
index.store_sdist_metadata(pkg, ver, sdist_pkg_info)
|
|
145
|
+
if sdist_pyproject_toml is not None:
|
|
146
|
+
index.store_sdist_pyproject(pkg, ver, sdist_pyproject_toml)
|
|
147
|
+
return _done_event()
|
|
148
|
+
|
|
149
|
+
def _request_wheel_metadata(
|
|
150
|
+
pkg: str, ver: str, filename: str, _url: str
|
|
151
|
+
) -> threading.Event:
|
|
152
|
+
if filename in failures:
|
|
153
|
+
index.store_metadata(pkg, f"{ver}#{filename}", None)
|
|
154
|
+
return _done_event()
|
|
155
|
+
|
|
156
|
+
coordinator.request_sdist.side_effect = _request_sdist
|
|
157
|
+
coordinator.request_wheel_metadata.side_effect = _request_wheel_metadata
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def make_coordinator( # noqa: PLR0913
|
|
161
|
+
wheels: Sequence[WheelFile | SdistFile] | None = None,
|
|
162
|
+
*,
|
|
163
|
+
package: str = "pkg",
|
|
164
|
+
listings: Mapping[str, Sequence[WheelFile | SdistFile]] | None = None,
|
|
165
|
+
metadata_text: str | None = None,
|
|
166
|
+
metadata_by_version: Mapping[str, str | None] | None = None,
|
|
167
|
+
auto_metadata: bool = False,
|
|
168
|
+
sdist_pkg_info: str | None = None,
|
|
169
|
+
sdist_pyproject_toml: str | None = None,
|
|
170
|
+
baseline_metadata: Mapping[str, str] | None = None,
|
|
171
|
+
per_wheel_metadata: Mapping[str, str] | None = None,
|
|
172
|
+
sdist_pyproject: Mapping[str, str] | None = None,
|
|
173
|
+
fetch_failures: set[str] | None = None,
|
|
174
|
+
) -> MagicMock:
|
|
175
|
+
"""Build a mock :class:`FetchCoordinator` backed by an :class:`InMemoryIndex`.
|
|
176
|
+
|
|
177
|
+
Listing setup (one of):
|
|
178
|
+
|
|
179
|
+
* ``wheels`` + ``package``: pre-load ``wheels`` under ``package``.
|
|
180
|
+
Passing ``None`` skips listing setup, e.g. for tests that only
|
|
181
|
+
need the coordinator handle.
|
|
182
|
+
* ``listings``: pre-load each ``(package, wheels)`` pair. Overrides
|
|
183
|
+
``wheels``/``package``.
|
|
184
|
+
|
|
185
|
+
Request side effects:
|
|
186
|
+
|
|
187
|
+
* ``request_listing`` and ``request_wheel_metadata`` always return a
|
|
188
|
+
set event. ``request_wheel_metadata`` honours ``fetch_failures``:
|
|
189
|
+
filenames in the set store ``None`` at the sentinel
|
|
190
|
+
``f"{version}#{filename}"`` key.
|
|
191
|
+
* ``request_metadata`` and ``request_metadata_batch`` write
|
|
192
|
+
``metadata_text`` (or the entry from ``metadata_by_version``, or
|
|
193
|
+
auto-generated minimal METADATA when ``auto_metadata`` is true).
|
|
194
|
+
* ``request_sdist`` writes ``sdist_pkg_info`` and, if not ``None``,
|
|
195
|
+
``sdist_pyproject_toml``.
|
|
196
|
+
|
|
197
|
+
Pre-stores written directly to the index before the coordinator
|
|
198
|
+
fires:
|
|
199
|
+
|
|
200
|
+
* ``baseline_metadata`` keys on package name and writes once per
|
|
201
|
+
package using the first wheel's version.
|
|
202
|
+
* ``per_wheel_metadata`` keys on wheel filename and writes at the
|
|
203
|
+
validator's ``f"{version}#{filename}"`` sentinel.
|
|
204
|
+
* ``sdist_pyproject`` keys on package name and writes the
|
|
205
|
+
pyproject.toml text used by the PEP 621 fast path.
|
|
206
|
+
|
|
207
|
+
Call sites that need request side effects beyond what this helper
|
|
208
|
+
wires up (for example ``request_sdist_archive``) can reassign
|
|
209
|
+
``.side_effect`` on the returned mock; the index is exposed at
|
|
210
|
+
``coordinator.index`` for direct manipulation.
|
|
211
|
+
"""
|
|
212
|
+
index = InMemoryIndex()
|
|
213
|
+
failures = fetch_failures if fetch_failures is not None else set()
|
|
214
|
+
|
|
215
|
+
listings_map = _resolve_listings(wheels, package, listings)
|
|
216
|
+
_pre_populate_index(
|
|
217
|
+
index,
|
|
218
|
+
listings_map,
|
|
219
|
+
baseline_metadata=baseline_metadata,
|
|
220
|
+
per_wheel_metadata=per_wheel_metadata,
|
|
221
|
+
sdist_pyproject=sdist_pyproject,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
coordinator = MagicMock()
|
|
225
|
+
coordinator.index = index
|
|
226
|
+
|
|
227
|
+
resolve_metadata = _make_metadata_resolver(
|
|
228
|
+
metadata_text=metadata_text,
|
|
229
|
+
metadata_by_version=metadata_by_version,
|
|
230
|
+
auto_metadata=auto_metadata,
|
|
231
|
+
)
|
|
232
|
+
_wire_metadata_side_effects(coordinator, index, resolve_metadata)
|
|
233
|
+
_wire_sdist_side_effects(
|
|
234
|
+
coordinator,
|
|
235
|
+
index,
|
|
236
|
+
sdist_pkg_info=sdist_pkg_info,
|
|
237
|
+
sdist_pyproject_toml=sdist_pyproject_toml,
|
|
238
|
+
failures=failures,
|
|
239
|
+
)
|
|
240
|
+
return coordinator
|