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,156 @@
|
|
|
1
|
+
"""Decision-aware look-ahead for :class:`nab_python.provider.Provider`.
|
|
2
|
+
|
|
3
|
+
Owns ``_look_ahead_ok`` and the pending-block tables that record
|
|
4
|
+
"this candidate is incompatible with this decision/positive range"
|
|
5
|
+
rejections. Each rejection becomes a grouped binary
|
|
6
|
+
incompatibility (``{candidate range, decision==w}``) when
|
|
7
|
+
``flush_pending_blocks`` runs at the end of ``choose_version``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from nab_resolver.types import Incompatibility, IncompatibilityCause, Term
|
|
16
|
+
|
|
17
|
+
from .._vendor.packaging.ranges import VersionRange
|
|
18
|
+
from .._vendor.packaging.utils import canonicalize_name
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .._vendor.packaging.version import Version
|
|
22
|
+
from ..provider import Provider
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def look_ahead_ok(
|
|
26
|
+
provider: Provider,
|
|
27
|
+
package: str,
|
|
28
|
+
version: Version,
|
|
29
|
+
*,
|
|
30
|
+
check_decisions: bool = True,
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""Check candidate compatibility with root reqs and decisions.
|
|
33
|
+
|
|
34
|
+
With ``check_decisions=False`` only the root-requirement check runs;
|
|
35
|
+
used for "subsequent candidate" iterations to avoid per-candidate clause
|
|
36
|
+
growth on tight version-locks. Extras proxies are skipped (the base's
|
|
37
|
+
look-ahead is sufficient).
|
|
38
|
+
|
|
39
|
+
``MetadataError`` (including ``UnsupportedSdistError``) is treated as a
|
|
40
|
+
rejection so the resolver moves on; the message is recorded for the
|
|
41
|
+
eventual no-versions diagnostic.
|
|
42
|
+
"""
|
|
43
|
+
# Late import: pypi imports this module at module load.
|
|
44
|
+
from ..provider import MetadataError
|
|
45
|
+
|
|
46
|
+
if provider.split_and_normalize(package)[1] is not None:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
cache_key = (package, version)
|
|
50
|
+
if cache_key not in provider.deps_cache:
|
|
51
|
+
try:
|
|
52
|
+
provider.get_dependencies(package, version)
|
|
53
|
+
except MetadataError as exc:
|
|
54
|
+
provider.pending_metadata_blocks[canonicalize_name(package)].append(
|
|
55
|
+
(version, str(exc))
|
|
56
|
+
)
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
deps = provider.deps_cache.get(cache_key, {})
|
|
60
|
+
decisions = provider.solution_decisions if check_decisions else None
|
|
61
|
+
|
|
62
|
+
for dep_name, dep_range in deps.items():
|
|
63
|
+
dep_normalized = canonicalize_name(dep_name)
|
|
64
|
+
|
|
65
|
+
# Root-requirement disagreement: diagnostic-only (the resolver
|
|
66
|
+
# already has the clause via its root_requirements input).
|
|
67
|
+
if dep_normalized in provider.root_requirements:
|
|
68
|
+
root_range = provider.root_requirements[dep_normalized]
|
|
69
|
+
if (dep_range & root_range).is_empty:
|
|
70
|
+
provider.pending_root_blocks[
|
|
71
|
+
(package, dep_normalized, dep_range, root_range)
|
|
72
|
+
].append(version)
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
if decisions is not None:
|
|
76
|
+
decided_version = decisions.get(dep_normalized)
|
|
77
|
+
if decided_version is not None and decided_version not in dep_range:
|
|
78
|
+
provider.pending_blocks[
|
|
79
|
+
(package, dep_normalized, decided_version)
|
|
80
|
+
].append(version)
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
# Positive-range disagreement: {candidate==v, dep in pos_range}
|
|
84
|
+
# is impossible. Sound across backjumps because the
|
|
85
|
+
# ``dep in pos_range`` term goes UNDETERMINED if the supporting
|
|
86
|
+
# derivation is reverted.
|
|
87
|
+
pos_range = provider.solution_ranges.get(dep_normalized)
|
|
88
|
+
if (
|
|
89
|
+
pos_range is not None
|
|
90
|
+
and decided_version is None
|
|
91
|
+
and (dep_range & pos_range).is_empty
|
|
92
|
+
):
|
|
93
|
+
provider.pending_range_blocks[
|
|
94
|
+
(package, dep_normalized, pos_range)
|
|
95
|
+
].append(version)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def flush_pending_blocks(provider: Provider) -> None:
|
|
102
|
+
"""Convert queued rejections into grouped binary incompatibilities.
|
|
103
|
+
|
|
104
|
+
For each ``(candidate_pkg, blocker_pkg, blocker_version)`` group we add
|
|
105
|
+
``{candidate_pkg in {v1,v2,...}, blocker_pkg==w}``. Sound across
|
|
106
|
+
backjumps because the blocker term goes UNDETERMINED when the supporting
|
|
107
|
+
decision is reverted, so the candidate range can be reconsidered.
|
|
108
|
+
"""
|
|
109
|
+
# Decision-keyed rejections: pin the blocker to one exact version.
|
|
110
|
+
for (
|
|
111
|
+
candidate_pkg,
|
|
112
|
+
blocker_pkg,
|
|
113
|
+
blocker_version,
|
|
114
|
+
), versions in provider.pending_blocks.items():
|
|
115
|
+
range_union = VersionRange.empty()
|
|
116
|
+
for v in versions:
|
|
117
|
+
range_union = range_union | VersionRange.singleton(v)
|
|
118
|
+
provider.pending_clauses.append(
|
|
119
|
+
Incompatibility(
|
|
120
|
+
[
|
|
121
|
+
Term(candidate_pkg, range_union, positive=True),
|
|
122
|
+
Term(
|
|
123
|
+
blocker_pkg,
|
|
124
|
+
VersionRange.singleton(blocker_version),
|
|
125
|
+
positive=True,
|
|
126
|
+
),
|
|
127
|
+
],
|
|
128
|
+
cause=IncompatibilityCause.DEPENDENCY,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
provider.pending_blocks = defaultdict(list)
|
|
132
|
+
|
|
133
|
+
# Range-keyed rejections: the blocker term uses the positive range directly.
|
|
134
|
+
for (
|
|
135
|
+
candidate_pkg,
|
|
136
|
+
blocker_pkg,
|
|
137
|
+
blocker_range,
|
|
138
|
+
), versions in provider.pending_range_blocks.items():
|
|
139
|
+
range_union = VersionRange.empty()
|
|
140
|
+
for v in versions:
|
|
141
|
+
range_union = range_union | VersionRange.singleton(v)
|
|
142
|
+
provider.pending_clauses.append(
|
|
143
|
+
Incompatibility(
|
|
144
|
+
[
|
|
145
|
+
Term(candidate_pkg, range_union, positive=True),
|
|
146
|
+
Term(blocker_pkg, blocker_range, positive=True),
|
|
147
|
+
],
|
|
148
|
+
cause=IncompatibilityCause.DEPENDENCY,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
provider.pending_range_blocks = defaultdict(list)
|
|
152
|
+
|
|
153
|
+
# Root- and metadata-blocks are diagnostic-only; drop them without
|
|
154
|
+
# emitting clauses.
|
|
155
|
+
provider.pending_root_blocks = defaultdict(list)
|
|
156
|
+
provider.pending_metadata_blocks = defaultdict(list)
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Metadata fetching, parsing, and dep classification for the provider.
|
|
2
|
+
|
|
3
|
+
Owns the bulk of ``get_dependencies``'s implementation: fetching
|
|
4
|
+
wheel METADATA / sdist PKG-INFO via the coordinator, parsing it
|
|
5
|
+
into a :class:`~nab_python.metadata.WheelMetadata`, and classifying
|
|
6
|
+
each ``Requires-Dist`` entry into base deps vs per-extra deps.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from nab_index.client import SdistFile, WheelFile
|
|
14
|
+
|
|
15
|
+
from .._vendor.packaging.ranges import VersionRange
|
|
16
|
+
from .._vendor.packaging.requirements import InvalidRequirement, Requirement
|
|
17
|
+
from .._vendor.packaging.utils import canonicalize_name
|
|
18
|
+
from ..metadata import DEPENDENCY_FIELDS, parse_metadata
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from .._vendor.packaging.markers import Marker
|
|
24
|
+
from .._vendor.packaging.version import Version
|
|
25
|
+
from ..metadata import WheelMetadata
|
|
26
|
+
from ..provider import DistFile, Provider
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_metadata(
|
|
30
|
+
provider: Provider,
|
|
31
|
+
versions: list[tuple[Version, DistFile]],
|
|
32
|
+
package: str,
|
|
33
|
+
version: Version,
|
|
34
|
+
) -> tuple[str, bool]:
|
|
35
|
+
"""Get metadata text and whether it came from an sdist.
|
|
36
|
+
|
|
37
|
+
Returns ``(metadata_text, from_sdist)``. ``from_sdist`` is
|
|
38
|
+
``True`` when the text was extracted from a source-distribution
|
|
39
|
+
``PKG-INFO`` rather than a wheel ``METADATA``: needed because
|
|
40
|
+
only sdist values are subject to the :pep:`643` Dynamic
|
|
41
|
+
guarantees and may need a ``pyproject.toml`` fallback.
|
|
42
|
+
"""
|
|
43
|
+
# Late import: ``pypi`` imports this module at module load.
|
|
44
|
+
from ..provider import MetadataError
|
|
45
|
+
|
|
46
|
+
_, _, normalized = provider.split_and_normalize(package)
|
|
47
|
+
ver_str = str(version)
|
|
48
|
+
|
|
49
|
+
text = provider.coordinator.index.get_metadata(normalized, ver_str)
|
|
50
|
+
if text is not None:
|
|
51
|
+
# Decide source from listing: a wheel-with-metadata-url at this
|
|
52
|
+
# version means the text was wheel METADATA; otherwise sdist
|
|
53
|
+
# PKG-INFO. ``_metadata`` and ``_sdist`` write to the same
|
|
54
|
+
# slot, so we can't tell from the index alone.
|
|
55
|
+
from_sdist = not has_wheel_metadata_at(versions, version)
|
|
56
|
+
return (text, from_sdist)
|
|
57
|
+
|
|
58
|
+
dist = pick_dist_for_metadata(versions, version)
|
|
59
|
+
if dist is None:
|
|
60
|
+
msg = f"Version {version} of {package} not found in listing"
|
|
61
|
+
raise MetadataError(msg)
|
|
62
|
+
|
|
63
|
+
if isinstance(dist, WheelFile) and dist.metadata_url is not None:
|
|
64
|
+
event = provider.coordinator.request_metadata(
|
|
65
|
+
normalized, ver_str, dist.metadata_url
|
|
66
|
+
)
|
|
67
|
+
event.wait()
|
|
68
|
+
metadata_text = provider.coordinator.index.get_metadata(normalized, ver_str)
|
|
69
|
+
else:
|
|
70
|
+
metadata_text = None
|
|
71
|
+
|
|
72
|
+
if metadata_text is not None:
|
|
73
|
+
return (metadata_text, False)
|
|
74
|
+
|
|
75
|
+
sdist = find_sdist(versions, version)
|
|
76
|
+
if sdist is not None:
|
|
77
|
+
metadata_text = fetch_sdist_metadata(provider, normalized, ver_str, sdist)
|
|
78
|
+
if metadata_text is not None:
|
|
79
|
+
return (metadata_text, True)
|
|
80
|
+
|
|
81
|
+
msg = (
|
|
82
|
+
f"No metadata for {package}=={version}: "
|
|
83
|
+
f"no PEP 658 metadata and no sdist available"
|
|
84
|
+
)
|
|
85
|
+
raise MetadataError(msg)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def has_wheel_metadata_at(
|
|
89
|
+
versions: Sequence[tuple[Version, DistFile]], version: Version
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""Report whether the listing has a wheel with PEP 658 metadata."""
|
|
92
|
+
for v, d in versions:
|
|
93
|
+
if v != version:
|
|
94
|
+
continue
|
|
95
|
+
if isinstance(d, WheelFile) and d.metadata_url is not None:
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def pick_dist_for_metadata(
|
|
101
|
+
versions: Sequence[tuple[Version, DistFile]], version: Version
|
|
102
|
+
) -> DistFile | None:
|
|
103
|
+
"""Pick the cheapest dist source for ``version``'s metadata.
|
|
104
|
+
|
|
105
|
+
Preference order at the same version:
|
|
106
|
+
|
|
107
|
+
1. A wheel with a PEP 658 ``metadata_url`` (smallest fetch).
|
|
108
|
+
2. Any wheel (range-fetch / stream still beats an sdist build).
|
|
109
|
+
3. The sdist (PKG-INFO; may require a build if Dynamic).
|
|
110
|
+
|
|
111
|
+
The picker is policy-agnostic and applies whatever ``versions``
|
|
112
|
+
holds. :attr:`~nab_python.provider.DistPolicy.SDIST_INSTALL` works
|
|
113
|
+
by keeping both kinds of dists in the listing so this preference
|
|
114
|
+
order naturally chooses the wheel when one exists, falling back
|
|
115
|
+
to the sdist when only the sdist is published.
|
|
116
|
+
"""
|
|
117
|
+
wheel_with_meta: DistFile | None = None
|
|
118
|
+
wheel_without_meta: DistFile | None = None
|
|
119
|
+
sdist: DistFile | None = None
|
|
120
|
+
for v, d in versions:
|
|
121
|
+
if v != version:
|
|
122
|
+
continue
|
|
123
|
+
if isinstance(d, WheelFile):
|
|
124
|
+
if d.metadata_url is not None:
|
|
125
|
+
if wheel_with_meta is None:
|
|
126
|
+
wheel_with_meta = d
|
|
127
|
+
elif wheel_without_meta is None:
|
|
128
|
+
wheel_without_meta = d
|
|
129
|
+
elif sdist is None:
|
|
130
|
+
sdist = d
|
|
131
|
+
return wheel_with_meta or wheel_without_meta or sdist
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_dynamic_deps(metadata: WheelMetadata) -> bool:
|
|
135
|
+
"""Return True when dependency fields are :pep:`643` Dynamic."""
|
|
136
|
+
return bool(DEPENDENCY_FIELDS & metadata.dynamic)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def resolve_dynamic_sdist(
|
|
140
|
+
provider: Provider,
|
|
141
|
+
cache_key: tuple[str, Version],
|
|
142
|
+
metadata: WheelMetadata,
|
|
143
|
+
) -> WheelMetadata:
|
|
144
|
+
"""Reconcile a dynamic-deps sdist.
|
|
145
|
+
|
|
146
|
+
First the bundled ``pyproject.toml`` is consulted; when its
|
|
147
|
+
``[project]`` table statically declares ``dependencies`` and
|
|
148
|
+
``optional-dependencies``, those replace the dynamic PKG-INFO
|
|
149
|
+
values. When that fallback yields nothing and the effective
|
|
150
|
+
:class:`~nab_python.provider.BuildPolicy` is
|
|
151
|
+
:attr:`~nab_python.provider.BuildPolicy.BUILD_REMOTE`, the sdist is
|
|
152
|
+
fetched, extracted, and handed to a PEP 517 backend by
|
|
153
|
+
:func:`nab_python._provider.build_remote.build_remote_sdist`. Any
|
|
154
|
+
other effective policy raises
|
|
155
|
+
:class:`~nab_python.provider.UnsupportedSdistError`; the resolver
|
|
156
|
+
skips the version via
|
|
157
|
+
:func:`nab_python._provider.lookahead.look_ahead_ok` and surfaces the
|
|
158
|
+
accumulated reasons if no candidate ultimately works.
|
|
159
|
+
"""
|
|
160
|
+
# Late import: ``pypi`` imports this module at module load.
|
|
161
|
+
from ..provider import BuildPolicy, UnsupportedSdistError
|
|
162
|
+
from .build_remote import build_remote_sdist
|
|
163
|
+
|
|
164
|
+
package, version = cache_key
|
|
165
|
+
canonical = canonicalize_name(package)
|
|
166
|
+
version_str = str(version)
|
|
167
|
+
index = provider.coordinator.index
|
|
168
|
+
|
|
169
|
+
cached = index.get_resolved_sdist_metadata(canonical, version_str)
|
|
170
|
+
if cached is not None:
|
|
171
|
+
return cached
|
|
172
|
+
|
|
173
|
+
augmented = augment_from_pyproject(provider, package, version, metadata)
|
|
174
|
+
if augmented is not None:
|
|
175
|
+
index.store_resolved_sdist_metadata(canonical, version_str, augmented)
|
|
176
|
+
return augmented
|
|
177
|
+
effective = provider.effective_build_policy(canonical)
|
|
178
|
+
if effective is BuildPolicy.BUILD_REMOTE:
|
|
179
|
+
built = build_remote_sdist(provider, package, version)
|
|
180
|
+
index.store_resolved_sdist_metadata(canonical, version_str, built)
|
|
181
|
+
return built
|
|
182
|
+
provider.stats.excluded_by_build_policy += 1
|
|
183
|
+
msg = (
|
|
184
|
+
f"{package}=={version} sdist has dynamic dependencies and no static"
|
|
185
|
+
f" pyproject.toml fallback; building requires BuildPolicy.BUILD_REMOTE"
|
|
186
|
+
f" but the effective policy is {effective.value}"
|
|
187
|
+
)
|
|
188
|
+
raise UnsupportedSdistError(msg)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def augment_from_pyproject(
|
|
192
|
+
provider: Provider,
|
|
193
|
+
package: str,
|
|
194
|
+
version: Version,
|
|
195
|
+
metadata: WheelMetadata,
|
|
196
|
+
) -> WheelMetadata | None:
|
|
197
|
+
"""Replace dynamic deps with statically-declared pyproject deps.
|
|
198
|
+
|
|
199
|
+
Returns the augmented metadata, or ``None`` if pyproject.toml
|
|
200
|
+
is missing, malformed, or itself marks deps dynamic via
|
|
201
|
+
``[project].dynamic``.
|
|
202
|
+
"""
|
|
203
|
+
# Late import keeps the resolver-time path off ``WheelMetadata``
|
|
204
|
+
# construction unless the dynamic-deps pyproject fallback fires.
|
|
205
|
+
from ..metadata import WheelMetadata as _WheelMetadata
|
|
206
|
+
from ..metadata import load_static_project
|
|
207
|
+
|
|
208
|
+
text = provider.coordinator.index.get_sdist_pyproject(package, str(version))
|
|
209
|
+
project = load_static_project(text) if text is not None else None
|
|
210
|
+
if project is None:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
deps_field = project.get("dependencies")
|
|
214
|
+
if deps_field is not None and not isinstance(deps_field, list):
|
|
215
|
+
return None
|
|
216
|
+
optional = project.get("optional-dependencies")
|
|
217
|
+
if optional is not None and not isinstance(optional, dict):
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
requires_dist = list(parse_pyproject_deps(deps_field or []))
|
|
221
|
+
provides_extra = extend_with_extras(requires_dist, optional or {})
|
|
222
|
+
|
|
223
|
+
provider.stats.sdist_pyproject_fallbacks += 1
|
|
224
|
+
return _WheelMetadata(
|
|
225
|
+
name=metadata.name,
|
|
226
|
+
version=metadata.version,
|
|
227
|
+
requires_python=metadata.requires_python,
|
|
228
|
+
requires_dist=requires_dist,
|
|
229
|
+
provides_extra=provides_extra,
|
|
230
|
+
metadata_version=metadata.metadata_version,
|
|
231
|
+
dynamic=metadata.dynamic,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def extend_with_extras(requires_dist: list[Requirement], optional: dict) -> list[str]:
|
|
236
|
+
"""Append extras-gated requirements and return Provides-Extra names."""
|
|
237
|
+
# Late import: ``pypi`` imports this module at module load.
|
|
238
|
+
from ..provider import _add_extra_marker
|
|
239
|
+
|
|
240
|
+
provides_extra: list[str] = []
|
|
241
|
+
for extra_name, extra_deps in optional.items():
|
|
242
|
+
if not isinstance(extra_deps, list):
|
|
243
|
+
continue
|
|
244
|
+
provides_extra.append(extra_name)
|
|
245
|
+
for dep_str in extra_deps:
|
|
246
|
+
if not isinstance(dep_str, str):
|
|
247
|
+
continue
|
|
248
|
+
with_marker = _add_extra_marker(dep_str, extra_name)
|
|
249
|
+
try:
|
|
250
|
+
requires_dist.append(Requirement(with_marker))
|
|
251
|
+
except InvalidRequirement:
|
|
252
|
+
continue
|
|
253
|
+
return provides_extra
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def parse_pyproject_deps(deps: list) -> list[Requirement]:
|
|
257
|
+
"""Parse a ``project.dependencies`` list, dropping malformed entries."""
|
|
258
|
+
out: list[Requirement] = []
|
|
259
|
+
for dep_str in deps:
|
|
260
|
+
if not isinstance(dep_str, str):
|
|
261
|
+
continue
|
|
262
|
+
try:
|
|
263
|
+
out.append(Requirement(dep_str))
|
|
264
|
+
except InvalidRequirement:
|
|
265
|
+
continue
|
|
266
|
+
return out
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def find_sdist(
|
|
270
|
+
versions: list[tuple[Version, DistFile]],
|
|
271
|
+
version: Version,
|
|
272
|
+
) -> SdistFile | None:
|
|
273
|
+
"""Find an sdist for a specific version, or None."""
|
|
274
|
+
for v, d in versions:
|
|
275
|
+
if v == version and isinstance(d, SdistFile):
|
|
276
|
+
return d
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def fetch_sdist_metadata(
|
|
281
|
+
provider: Provider, package: str, version: str, sdist: SdistFile
|
|
282
|
+
) -> str | None:
|
|
283
|
+
"""Block until the coordinator returns sdist PKG-INFO text."""
|
|
284
|
+
event = provider.coordinator.request_sdist(package, version, sdist.url)
|
|
285
|
+
event.wait()
|
|
286
|
+
provider.stats.sdist_pkg_info_fetched += 1
|
|
287
|
+
return provider.coordinator.index.get_metadata(package, version)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def classify_requirement(
|
|
291
|
+
provider: Provider,
|
|
292
|
+
req: Requirement,
|
|
293
|
+
provided_extras: set[str],
|
|
294
|
+
) -> set[str] | None:
|
|
295
|
+
"""Classify a requirement by which extras it belongs to.
|
|
296
|
+
|
|
297
|
+
Returns None if the marker doesn't match the environment.
|
|
298
|
+
Returns an empty set if the requirement is a base dep (no extra gating).
|
|
299
|
+
Returns a set of normalized extra names if extra-gated.
|
|
300
|
+
"""
|
|
301
|
+
marker = req.marker
|
|
302
|
+
if marker is None:
|
|
303
|
+
return set()
|
|
304
|
+
marker_id = id(marker)
|
|
305
|
+
if marker_matches_base(provider, marker, marker_id):
|
|
306
|
+
return set()
|
|
307
|
+
if "extra" not in marker_text(provider, marker, marker_id):
|
|
308
|
+
return None
|
|
309
|
+
matched_extras = marker_matched_extras(provider, marker, marker_id, provided_extras)
|
|
310
|
+
return matched_extras or None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def marker_matches_base(provider: Provider, marker: Marker, marker_id: int) -> bool:
|
|
314
|
+
"""Evaluate ``marker`` against the env without ``extra`` set, cached."""
|
|
315
|
+
result = provider.marker_base_cache.get(marker_id)
|
|
316
|
+
if result is None:
|
|
317
|
+
result = marker.evaluate(provider.environment)
|
|
318
|
+
provider.marker_base_cache[marker_id] = result
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def marker_text(provider: Provider, marker: Marker, marker_id: int) -> str:
|
|
323
|
+
"""Return ``str(marker)``, cached. Walks the AST on big graphs."""
|
|
324
|
+
text = provider.marker_text_cache.get(marker_id)
|
|
325
|
+
if text is None:
|
|
326
|
+
text = str(marker)
|
|
327
|
+
provider.marker_text_cache[marker_id] = text
|
|
328
|
+
return text
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def marker_matched_extras(
|
|
332
|
+
provider: Provider,
|
|
333
|
+
marker: Marker,
|
|
334
|
+
marker_id: int,
|
|
335
|
+
provided_extras: set[str],
|
|
336
|
+
) -> set[str]:
|
|
337
|
+
"""Return the extras for which the marker evaluates to True."""
|
|
338
|
+
per_marker = provider.marker_extra_cache.get(marker_id)
|
|
339
|
+
if per_marker is None:
|
|
340
|
+
per_marker = provider.marker_extra_cache[marker_id] = {}
|
|
341
|
+
env = provider.env_with_extra
|
|
342
|
+
matched: set[str] = set()
|
|
343
|
+
for extra_name in provided_extras:
|
|
344
|
+
result = per_marker.get(extra_name)
|
|
345
|
+
if result is None:
|
|
346
|
+
env["extra"] = extra_name
|
|
347
|
+
result = marker.evaluate(env)
|
|
348
|
+
per_marker[extra_name] = result
|
|
349
|
+
if result:
|
|
350
|
+
matched.add(extra_name)
|
|
351
|
+
return matched
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def parse_and_cache_metadata(
|
|
355
|
+
provider: Provider,
|
|
356
|
+
cache_key: tuple[str, Version],
|
|
357
|
+
metadata_text: str,
|
|
358
|
+
*,
|
|
359
|
+
from_sdist: bool = False,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Parse metadata text and pre-compute per-extra deps.
|
|
362
|
+
|
|
363
|
+
Evaluates markers once for all extras, then caches the base
|
|
364
|
+
deps and a per-extra mapping so that get_extra_dependencies
|
|
365
|
+
can do a dict lookup instead of re-iterating requires_dist.
|
|
366
|
+
|
|
367
|
+
When ``from_sdist`` is set and PKG-INFO marks dependency-related
|
|
368
|
+
fields as :pep:`643` Dynamic, attempts the ``pyproject.toml``
|
|
369
|
+
fallback before raising :class:`UnsupportedSdistError` under
|
|
370
|
+
:class:`BuildPolicy.NEVER`.
|
|
371
|
+
|
|
372
|
+
The parsed :class:`WheelMetadata` is shared via the
|
|
373
|
+
:class:`~nab_python.fetch.InMemoryIndex` so that universal-mode
|
|
374
|
+
resolves only run :func:`parse_metadata` once per
|
|
375
|
+
``(package, version)`` regardless of how many tuples ask for it.
|
|
376
|
+
Per-tuple classification (marker evaluation, extras admission)
|
|
377
|
+
still runs locally in :func:`cache_deps_from_metadata`. The
|
|
378
|
+
sdist-dynamic-deps reconciliation in
|
|
379
|
+
:func:`resolve_dynamic_sdist` returns a *new* dataclass and is
|
|
380
|
+
therefore cached only by the (per-provider) ``metadata_cache``;
|
|
381
|
+
the coordinator-level entry stays the raw parse so subsequent
|
|
382
|
+
tuples can re-apply their own dynamic-resolution rules without
|
|
383
|
+
inheriting this tuple's pyproject fallback.
|
|
384
|
+
"""
|
|
385
|
+
package, version = cache_key
|
|
386
|
+
version_str = str(version)
|
|
387
|
+
metadata = provider.coordinator.index.get_parsed_metadata(package, version_str)
|
|
388
|
+
if metadata is None:
|
|
389
|
+
metadata = parse_metadata(metadata_text)
|
|
390
|
+
provider.coordinator.index.store_parsed_metadata(package, version_str, metadata)
|
|
391
|
+
if from_sdist and is_dynamic_deps(metadata):
|
|
392
|
+
metadata = resolve_dynamic_sdist(provider, cache_key, metadata)
|
|
393
|
+
cache_deps_from_metadata(provider, cache_key, metadata)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def cache_deps_from_metadata(
|
|
397
|
+
provider: Provider,
|
|
398
|
+
cache_key: tuple[str, Version],
|
|
399
|
+
metadata: WheelMetadata,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Populate ``deps_cache`` + ``extra_deps_map`` from a parsed metadata.
|
|
402
|
+
|
|
403
|
+
Shared by the wheel/sdist path (which calls
|
|
404
|
+
:func:`parse_and_cache_metadata` after parsing METADATA text)
|
|
405
|
+
and the local-source path (which already has a
|
|
406
|
+
:class:`WheelMetadata` from
|
|
407
|
+
:func:`nab_python.build_backend.extract_static_metadata`).
|
|
408
|
+
"""
|
|
409
|
+
# Late import: ``pypi`` imports this module at module load.
|
|
410
|
+
from ..provider import _normalize_extra
|
|
411
|
+
|
|
412
|
+
provider.metadata_cache[cache_key] = metadata
|
|
413
|
+
provided_extras = {_normalize_extra(e) for e in metadata.provides_extra}
|
|
414
|
+
base_deps: dict[str, VersionRange] = {}
|
|
415
|
+
extra_deps_map: dict[str, dict[str, VersionRange]] = {
|
|
416
|
+
e: {} for e in provided_extras
|
|
417
|
+
}
|
|
418
|
+
for req in metadata.requires_dist:
|
|
419
|
+
req_extras = classify_requirement(provider, req, provided_extras)
|
|
420
|
+
if req_extras is None:
|
|
421
|
+
continue
|
|
422
|
+
add_classified_dep(req, req_extras, base_deps, extra_deps_map)
|
|
423
|
+
provider.deps_cache[cache_key] = base_deps
|
|
424
|
+
provider.extra_deps_map[cache_key] = extra_deps_map
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def add_classified_dep(
|
|
428
|
+
req: Requirement,
|
|
429
|
+
req_extras: set[str],
|
|
430
|
+
base_deps: dict[str, VersionRange],
|
|
431
|
+
extra_deps_map: dict[str, dict[str, VersionRange]],
|
|
432
|
+
) -> None:
|
|
433
|
+
"""Add a classified requirement to the appropriate dep set."""
|
|
434
|
+
# Late import: ``pypi`` imports this module at module load.
|
|
435
|
+
from ..provider import join_extra
|
|
436
|
+
|
|
437
|
+
name = canonicalize_name(req.name)
|
|
438
|
+
vi = req.specifier.to_range()
|
|
439
|
+
dep_extras: set[str] = req.extras
|
|
440
|
+
|
|
441
|
+
if not req_extras:
|
|
442
|
+
base_deps[name] = vi
|
|
443
|
+
for re in dep_extras:
|
|
444
|
+
base_deps[join_extra(name, re)] = VersionRange.full()
|
|
445
|
+
else:
|
|
446
|
+
for extra_name in req_extras:
|
|
447
|
+
edeps = extra_deps_map[extra_name]
|
|
448
|
+
edeps[name] = vi
|
|
449
|
+
for re in dep_extras:
|
|
450
|
+
edeps[join_extra(name, re)] = VersionRange.full()
|