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.
Files changed (71) hide show
  1. nab_python/__init__.py +1 -0
  2. nab_python/_build/__init__.py +1 -0
  3. nab_python/_build/env.py +364 -0
  4. nab_python/_build/errors.py +17 -0
  5. nab_python/_build/runner.py +254 -0
  6. nab_python/_lockfile/__init__.py +1 -0
  7. nab_python/_lockfile/builder.py +339 -0
  8. nab_python/_lockfile/disjointness.py +207 -0
  9. nab_python/_lockfile/pylock.py +323 -0
  10. nab_python/_lockfile/requirements.py +121 -0
  11. nab_python/_packaging_provider.py +98 -0
  12. nab_python/_provider/__init__.py +1 -0
  13. nab_python/_provider/build_remote.py +95 -0
  14. nab_python/_provider/extras.py +231 -0
  15. nab_python/_provider/listing.py +442 -0
  16. nab_python/_provider/lookahead.py +156 -0
  17. nab_python/_provider/metadata_resolver.py +450 -0
  18. nab_python/_provider/priority.py +174 -0
  19. nab_python/_provider/sources.py +215 -0
  20. nab_python/_testing/__init__.py +1 -0
  21. nab_python/_testing/coordinator_fake.py +240 -0
  22. nab_python/_vcs_admission.py +209 -0
  23. nab_python/_vendor/__init__.py +6 -0
  24. nab_python/_vendor/packaging/LICENSE +3 -0
  25. nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  26. nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  27. nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  28. nab_python/_vendor/packaging/__init__.py +15 -0
  29. nab_python/_vendor/packaging/_elffile.py +108 -0
  30. nab_python/_vendor/packaging/_manylinux.py +265 -0
  31. nab_python/_vendor/packaging/_musllinux.py +88 -0
  32. nab_python/_vendor/packaging/_parser.py +394 -0
  33. nab_python/_vendor/packaging/_structures.py +33 -0
  34. nab_python/_vendor/packaging/_tokenizer.py +196 -0
  35. nab_python/_vendor/packaging/dependency_groups.py +302 -0
  36. nab_python/_vendor/packaging/direct_url.py +325 -0
  37. nab_python/_vendor/packaging/errors.py +94 -0
  38. nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  39. nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  40. nab_python/_vendor/packaging/markers.py +506 -0
  41. nab_python/_vendor/packaging/metadata.py +964 -0
  42. nab_python/_vendor/packaging/py.typed +0 -0
  43. nab_python/_vendor/packaging/pylock.py +910 -0
  44. nab_python/_vendor/packaging/ranges.py +1803 -0
  45. nab_python/_vendor/packaging/requirements.py +132 -0
  46. nab_python/_vendor/packaging/specifiers.py +1141 -0
  47. nab_python/_vendor/packaging/tags.py +929 -0
  48. nab_python/_vendor/packaging/utils.py +296 -0
  49. nab_python/_vendor/packaging/version.py +1230 -0
  50. nab_python/build_backend.py +184 -0
  51. nab_python/config.py +805 -0
  52. nab_python/download.py +170 -0
  53. nab_python/fetch.py +827 -0
  54. nab_python/lockfile.py +238 -0
  55. nab_python/metadata.py +145 -0
  56. nab_python/provider.py +1235 -0
  57. nab_python/py.typed +0 -0
  58. nab_python/requirements_file.py +180 -0
  59. nab_python/resolve.py +497 -0
  60. nab_python/universal/__init__.py +1 -0
  61. nab_python/universal/matrix.py +235 -0
  62. nab_python/universal/provider.py +214 -0
  63. nab_python/universal/reresolve.py +310 -0
  64. nab_python/universal/resolve.py +508 -0
  65. nab_python/universal/validate.py +439 -0
  66. nab_python/universal/wheel_selection.py +327 -0
  67. nab_python/workspace.py +214 -0
  68. nab_python-0.0.1.dist-info/METADATA +49 -0
  69. nab_python-0.0.1.dist-info/RECORD +71 -0
  70. nab_python-0.0.1.dist-info/WHEEL +4 -0
  71. nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,327 @@
1
+ """Predict which wheel a ``(python_version, platform_id)`` tuple would install.
2
+
3
+ Universal resolution needs the install-time wheel selection answer
4
+ without a live interpreter, so the tag set is computed from
5
+ :class:`PlatformSpec` directly. CPython tags come from
6
+ ``packaging.tags.cpython_tags``, interpreter-agnostic tags from
7
+ ``compatible_tags``, macOS from ``mac_platforms``, and manylinux /
8
+ musllinux are expanded from a declared glibc / musl floor. A wheel
9
+ matches the tuple iff its parsed tags share a member with the
10
+ tuple's compatible-tag set.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from functools import cache, lru_cache
17
+ from typing import TYPE_CHECKING
18
+
19
+ from .._vendor.packaging import tags as ptags
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Iterable
23
+
24
+ from nab_index.client import WheelFile
25
+
26
+ from .._vendor.packaging.tags import Tag
27
+
28
+
29
+ # PEP 427: a wheel filename has at least 5 dash-separated segments
30
+ # (name-version-pythontag-abitag-platformtag.whl), or 6 with a build tag.
31
+ __all__ = [
32
+ "PlatformSpec",
33
+ "compatible_tags_for_tuple",
34
+ "select_wheel_for_tuple",
35
+ "wheel_compatible_with_tuple",
36
+ ]
37
+
38
+
39
+ _MIN_WHEEL_FILENAME_PARTS = 5
40
+
41
+ # Default manylinux floor: glibc 2.17 (the manylinux2014 generation,
42
+ # adopted by every mainstream distro since CentOS 7 / Ubuntu 14.04).
43
+ # A tighter floor reduces accepted wheels; a looser floor accepts
44
+ # wheels that may not run on older glibc.
45
+ _DEFAULT_MANYLINUX_FLOOR = (2, 17)
46
+ # Default musllinux floor: musl 1.2 (adopted by Alpine 3.13+, 2021+).
47
+ _DEFAULT_MUSLLINUX_FLOOR = (1, 2)
48
+ # Default macOS minimum: 11 (Big Sur, 2020+). arm64 was introduced
49
+ # at 11.0; using 10.x for arm64 has no compatible wheels.
50
+ _DEFAULT_MACOS_MIN = (11, 0)
51
+ # Default macOS minimum for x86_64 builds. 10.13 was the last with
52
+ # wide wheel coverage; newer macOS x86_64 builds rarely declare
53
+ # below 10.13.
54
+ _DEFAULT_MACOS_X86_64_MIN = (10, 13)
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class PlatformSpec:
59
+ """Concrete tag floors for one matrix platform_id.
60
+
61
+ Users can override the per-platform floors when their
62
+ deployment target requires it. The defaults are deliberately
63
+ permissive (manylinux 2.17, musl 1.2, macOS 11) so most real
64
+ deployments work out of the box.
65
+
66
+ ``platform_release`` and ``platform_version`` set the
67
+ corresponding PEP 508 marker values on this platform's tuples
68
+ (hole 1.3 plug). When unset, both default to the empty string,
69
+ which makes any kernel-version-conditioned marker
70
+ (``platform_release >= "5.10"``) evaluate False (the safe
71
+ direction: drop the gated dep) but a silent failure if the
72
+ target machine actually has that kernel. Users who declare a
73
+ minimum target kernel get the gated deps included.
74
+ """
75
+
76
+ platform_id: str
77
+ manylinux_floor: tuple[int, int] = _DEFAULT_MANYLINUX_FLOOR
78
+ musllinux_floor: tuple[int, int] = _DEFAULT_MUSLLINUX_FLOOR
79
+ macos_min: tuple[int, int] | None = None # arch-dependent default
80
+ platform_release: str = ""
81
+ platform_version: str = ""
82
+
83
+ @property
84
+ def arch(self) -> str:
85
+ """The architecture suffix used in platform tags."""
86
+ return _PLATFORM_ARCH[self.platform_id]
87
+
88
+
89
+ # Map our matrix platform_ids to (kind, arch). Kind is one of
90
+ # "linux", "macos", "windows". Used for tag generation.
91
+ _PLATFORM_ARCH: dict[str, str] = {
92
+ "linux_x86_64": "x86_64",
93
+ "linux_aarch64": "aarch64",
94
+ "macos_arm64": "arm64",
95
+ "macos_x86_64": "x86_64",
96
+ "windows_amd64": "amd64",
97
+ }
98
+
99
+ _PLATFORM_KIND: dict[str, str] = {
100
+ "linux_x86_64": "linux",
101
+ "linux_aarch64": "linux",
102
+ "macos_arm64": "macos",
103
+ "macos_x86_64": "macos",
104
+ "windows_amd64": "windows",
105
+ }
106
+
107
+
108
+ def _linux_platform_tags(
109
+ arch: str,
110
+ *,
111
+ manylinux_floor: tuple[int, int],
112
+ musllinux_floor: tuple[int, int],
113
+ ) -> list[str]:
114
+ """Generate manylinux + musllinux + plain linux tags for an arch.
115
+
116
+ Returns the tag list in install-preference order: most-specific
117
+ (highest glibc/musl version) first. Accepts every minor version
118
+ at or below the declared floor; this is the spec-compliant
119
+ interpretation of "manylinux_X_Y means glibc X.Y or older".
120
+
121
+ Note: PEP 600 says installers prefer wheels with the *highest*
122
+ glibc among compatible ones. Our use case is "decide which
123
+ wheel a tuple would install"; the tag list ordering matches
124
+ that preference.
125
+ """
126
+ # manylinux_X_Y: PEP 600 form. We accept any minor at or below
127
+ # the floor (a wheel built for glibc 2.5 runs on a system with
128
+ # glibc 2.17; a wheel built for glibc 2.34 does not). Iterate
129
+ # high-to-low for preference order.
130
+ major, minor = manylinux_floor
131
+ out = [f"manylinux_{major}_{m}_{arch}" for m in range(minor, -1, -1)]
132
+ # Legacy aliases (PEPs 513/571/599). These map to specific
133
+ # glibc versions: manylinux1=2.5, manylinux2010=2.12,
134
+ # manylinux2014=2.17. We include them when they're <= floor.
135
+ legacy_aliases = [
136
+ ("manylinux1", (2, 5)),
137
+ ("manylinux2010", (2, 12)),
138
+ ("manylinux2014", (2, 17)),
139
+ ]
140
+ out.extend(
141
+ f"{name}_{arch}" for name, lver in legacy_aliases if lver <= manylinux_floor
142
+ )
143
+ # musllinux_X_Y: PEP 656 form. Same accept-at-or-below rule.
144
+ mu_major, mu_minor = musllinux_floor
145
+ out.extend(f"musllinux_{mu_major}_{m}_{arch}" for m in range(mu_minor, -1, -1))
146
+ # Plain linux_<arch>: the most generic Linux tag. Most installers
147
+ # accept this only when no manylinux/musllinux wheel is present.
148
+ out.append(f"linux_{arch}")
149
+ return out
150
+
151
+
152
+ def _platform_tags_for_spec(spec: PlatformSpec) -> list[str]:
153
+ """Build the platform-tag list for ``spec`` in preference order."""
154
+ kind = _PLATFORM_KIND[spec.platform_id]
155
+ arch = spec.arch
156
+
157
+ if kind == "linux":
158
+ return _linux_platform_tags(
159
+ arch,
160
+ manylinux_floor=spec.manylinux_floor,
161
+ musllinux_floor=spec.musllinux_floor,
162
+ )
163
+
164
+ if kind == "macos":
165
+ macos_min = spec.macos_min
166
+ if macos_min is None:
167
+ macos_min = (
168
+ _DEFAULT_MACOS_MIN if arch == "arm64" else _DEFAULT_MACOS_X86_64_MIN
169
+ )
170
+ # mac_platforms treats the declared OS as a max and yields older too.
171
+ return list(ptags.mac_platforms(version=macos_min, arch=arch))
172
+
173
+ if kind == "windows":
174
+ return [f"win_{arch}"]
175
+
176
+ # Unreachable; PlatformSpec construction validates.
177
+ msg = f"Unknown platform kind: {kind}" # pragma: no cover
178
+ raise ValueError(msg) # pragma: no cover
179
+
180
+
181
+ @cache
182
+ def compatible_tags_for_tuple(
183
+ *,
184
+ python_version: str,
185
+ spec: PlatformSpec,
186
+ ) -> frozenset[Tag]:
187
+ """Return the full set of tags ``(python_version, spec)`` accepts.
188
+
189
+ Combines:
190
+
191
+ 1. CPython-specific tags via ``packaging.tags.cpython_tags``
192
+ (cpXY-cpXY, cpXY-abi3 forward-compat, cpXY-none).
193
+ 2. Interpreter-agnostic tags via ``packaging.tags.compatible_tags``
194
+ (pyXY-none-any, py3-none-any, etc.).
195
+
196
+ The platform list is computed by :func:`_platform_tags_for_spec`.
197
+ Cached on ``(python_version, spec)``: both inputs are immutable
198
+ (str, frozen dataclass) and the resulting set is identical across
199
+ every wheel-compatibility check for the same tuple, so the cache
200
+ skips rebuilding the same :class:`Tag` set per call.
201
+ """
202
+ major, minor = (int(p) for p in python_version.split("."))
203
+ py_version = (major, minor)
204
+ abi = f"cp{major}{minor}"
205
+ platforms = _platform_tags_for_spec(spec)
206
+ out: set[Tag] = set()
207
+ out.update(
208
+ ptags.cpython_tags(python_version=py_version, abis=[abi], platforms=platforms)
209
+ )
210
+ out.update(
211
+ ptags.compatible_tags(
212
+ python_version=py_version, interpreter=abi, platforms=platforms
213
+ )
214
+ )
215
+ return frozenset(out)
216
+
217
+
218
+ @lru_cache(maxsize=4096)
219
+ def _intern_tag(tag: Tag) -> Tag:
220
+ """Return a shared :class:`Tag` for ``tag``.
221
+
222
+ ``packaging.tags.parse_tag`` constructs fresh :class:`Tag` instances
223
+ on every call. The set of distinct (interpreter, abi, platform)
224
+ triples in a single PyPI scan is small compared with the wheels
225
+ visited, so sharing the canonical instance collapses the duplicates.
226
+ ``Tag`` is immutable (``__slots__``) so the shared instance is safe.
227
+ """
228
+ return tag
229
+
230
+
231
+ @lru_cache(maxsize=8192)
232
+ def _parse_tag_str(tag_str: str) -> frozenset[Tag] | None:
233
+ """Cache ``parse_tag`` keyed on the wheel's ``python-abi-platform``.
234
+
235
+ Many distinct wheel filenames share the same tag suffix
236
+ (e.g. ``cp310-cp310-manylinux2014_x86_64``), so caching by tag
237
+ string deduplicates more aggressively than caching by filename.
238
+ Returns ``None`` for unparseable input.
239
+ """
240
+ try:
241
+ raw = ptags.parse_tag(tag_str)
242
+ except Exception: # noqa: BLE001 - never trust upstream parser
243
+ return None
244
+ return frozenset(_intern_tag(t) for t in raw)
245
+
246
+
247
+ def wheel_tag_set(filename: str) -> frozenset[Tag] | None:
248
+ """Parse a wheel filename into the set of tags it advertises.
249
+
250
+ Per PEP 427 the filename's last three dash-separated segments
251
+ are ``python-abi-platform``; per PEP 425 each can be a
252
+ dot-separated compressed set. Returns ``None`` for a non-wheel
253
+ filename or one with too few segments. The expensive work
254
+ (``parse_tag`` + Tag interning) lives in :func:`_parse_tag_str`,
255
+ which is cached on the suffix so wheels that share it
256
+ short-circuit.
257
+ """
258
+ if not filename.endswith(".whl"):
259
+ return None
260
+ stem = filename[:-4]
261
+ parts = stem.split("-")
262
+ # PEP 427: filename has 5 segments (no build tag) or 6 (with build).
263
+ if len(parts) < _MIN_WHEEL_FILENAME_PARTS:
264
+ return None
265
+ # The last three dash-separated segments are python-abi-platform.
266
+ return _parse_tag_str("-".join(parts[-3:]))
267
+
268
+
269
+ def wheel_compatible_with_tuple(
270
+ wheel: WheelFile,
271
+ *,
272
+ python_version: str,
273
+ spec: PlatformSpec,
274
+ ) -> bool:
275
+ """Return True iff ``wheel`` is a candidate for the given tuple."""
276
+ wheel_tags = wheel_tag_set(wheel.filename)
277
+ if wheel_tags is None:
278
+ return False
279
+ compat = compatible_tags_for_tuple(python_version=python_version, spec=spec)
280
+ # ``frozenset.isdisjoint`` is a C-level builtin that beats the
281
+ # Python ``any(t in compat for t in wheel_tags)`` generator on
282
+ # the per-wheel hot loop.
283
+ return not wheel_tags.isdisjoint(compat)
284
+
285
+
286
+ def select_wheel_for_tuple(
287
+ wheels: Iterable[WheelFile],
288
+ *,
289
+ python_version: str,
290
+ spec: PlatformSpec,
291
+ ) -> WheelFile | None:
292
+ """Pick the most-specific compatible wheel for the tuple, or None.
293
+
294
+ Implements PEP 425 preference: wheels matching earlier
295
+ (more-specific) tags in ``compatible_tags_for_tuple`` win over
296
+ those matching later (more-generic) tags. Within the same tag
297
+ rank, the first wheel in input order wins.
298
+ """
299
+ compat_list = list(_compatible_tags_in_order(python_version, spec))
300
+ rank: dict[Tag, int] = {tag: i for i, tag in enumerate(compat_list)}
301
+
302
+ best: tuple[int, WheelFile] | None = None
303
+ for wheel in wheels:
304
+ wheel_tags = wheel_tag_set(wheel.filename)
305
+ if not wheel_tags:
306
+ continue
307
+ # Lowest rank index wins (most-specific tag).
308
+ wheel_rank = min((rank[t] for t in wheel_tags if t in rank), default=None)
309
+ if wheel_rank is None:
310
+ continue
311
+ if best is None or wheel_rank < best[0]:
312
+ best = (wheel_rank, wheel)
313
+ return best[1] if best is not None else None
314
+
315
+
316
+ def _compatible_tags_in_order(python_version: str, spec: PlatformSpec) -> Iterable[Tag]:
317
+ """Yield compatible tags in install preference order."""
318
+ major, minor = (int(p) for p in python_version.split("."))
319
+ py_version = (major, minor)
320
+ abi = f"cp{major}{minor}"
321
+ platforms = _platform_tags_for_spec(spec)
322
+ yield from ptags.cpython_tags(
323
+ python_version=py_version, abis=[abi], platforms=platforms
324
+ )
325
+ yield from ptags.compatible_tags(
326
+ python_version=py_version, interpreter=abi, platforms=platforms
327
+ )
@@ -0,0 +1,214 @@
1
+ """Workspace discovery for member ``pyproject.toml`` files.
2
+
3
+ A "workspace" here is the ``[tool.nab.workspace]`` table on a parent
4
+ ``pyproject.toml``. When ``nab lock`` is invoked against a member, this
5
+ module walks up to the root, reads the members, and synthesises a
6
+ :class:`~nab_python.provider.LocalSource` per member. The provider then
7
+ prefers those local sources over PyPI by canonical name, so a member
8
+ package resolves against its in-tree source instead of being fetched
9
+ from the index.
10
+
11
+ Members are listed literally; globs are refused with an error. Users
12
+ coming from other tools that allow globs get a clear migration
13
+ message.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING
21
+
22
+ import tomli
23
+
24
+ from ._vendor.packaging.utils import canonicalize_name
25
+ from .provider import BuildPolicy, LocalSource
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Iterable
29
+ from pathlib import Path
30
+
31
+
32
+ __all__ = [
33
+ "WorkspaceConfig",
34
+ "WorkspaceDiscoveryError",
35
+ "auto_promote_build_policy_for_workspace",
36
+ "discover_workspace_root",
37
+ "merge_workspace_local_sources",
38
+ "read_workspace_members",
39
+ ]
40
+
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ _PERMISSIVENESS = {
46
+ BuildPolicy.NEVER: 0,
47
+ BuildPolicy.BUILD_LOCAL: 1,
48
+ BuildPolicy.BUILD_REMOTE: 2,
49
+ }
50
+
51
+
52
+ class WorkspaceDiscoveryError(ValueError):
53
+ """Raised when a workspace member or root is structurally invalid."""
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class WorkspaceConfig:
58
+ """Parsed ``[tool.nab.workspace]`` table.
59
+
60
+ ``members`` is the literal list of paths declared at the workspace
61
+ root. No globs, no path resolution; those happen later in
62
+ :func:`read_workspace_members`.
63
+ """
64
+
65
+ members: tuple[str, ...]
66
+
67
+
68
+ def discover_workspace_root(member_pyproject: Path) -> Path | None:
69
+ """Return the workspace root pyproject for ``member_pyproject``.
70
+
71
+ Walks from ``member_pyproject``'s directory upwards looking for the
72
+ first ``pyproject.toml`` whose ``[tool.nab.workspace]`` table is
73
+ present. Returns that pyproject's path, or ``None`` when no such
74
+ ancestor (or self) exists.
75
+
76
+ The input ``member_pyproject`` is itself considered: a user invoking
77
+ ``nab lock`` on a workspace root sees discovery activate.
78
+ Filesystem-level errors and TOML parse errors during the walk are
79
+ swallowed: a malformed sibling pyproject should not prevent
80
+ discovery from finding a valid root above it.
81
+ """
82
+ start_dir = member_pyproject.resolve().parent
83
+ for parent in (start_dir, *start_dir.parents):
84
+ candidate = parent / "pyproject.toml"
85
+ if not candidate.is_file():
86
+ continue
87
+ try:
88
+ with candidate.open("rb") as f:
89
+ data = tomli.load(f)
90
+ except (OSError, tomli.TOMLDecodeError):
91
+ continue
92
+ if "workspace" in data.get("tool", {}).get("nab", {}):
93
+ return candidate
94
+ return None
95
+
96
+
97
+ def read_workspace_members(root_pyproject: Path) -> tuple[LocalSource, ...]:
98
+ """Synthesise :class:`LocalSource` entries from a workspace root.
99
+
100
+ Reads ``[tool.nab.workspace].members`` from ``root_pyproject``. Each
101
+ entry must be a literal path; any entry containing ``*``, ``?`` or
102
+ ``[`` raises :class:`WorkspaceDiscoveryError` with a message naming
103
+ the offending entry. For every member directory the function opens
104
+ ``<member>/pyproject.toml`` and requires ``[project].name``;
105
+ missing pyproject or missing name is a hard error.
106
+
107
+ Two members declaring the same canonical name raises
108
+ :class:`WorkspaceDiscoveryError`. The returned tuple preserves
109
+ declaration order, which makes ``nab lock`` output stable when the
110
+ list of members is itself stable.
111
+ """
112
+ with root_pyproject.open("rb") as f:
113
+ root_data = tomli.load(f)
114
+ raw_workspace = root_data.get("tool", {}).get("nab", {}).get("workspace")
115
+ if not isinstance(raw_workspace, dict):
116
+ msg = (
117
+ f"{root_pyproject}: [tool.nab.workspace] must be a table,"
118
+ f" got {type(raw_workspace).__name__}"
119
+ )
120
+ raise WorkspaceDiscoveryError(msg)
121
+ raw_members = raw_workspace.get("members")
122
+ if not isinstance(raw_members, list):
123
+ msg = (
124
+ f"{root_pyproject}: [tool.nab.workspace].members must be a list of"
125
+ f" strings, got {type(raw_members).__name__}"
126
+ )
127
+ raise WorkspaceDiscoveryError(msg)
128
+
129
+ sources: list[LocalSource] = []
130
+ seen: dict[str, str] = {}
131
+ for entry in raw_members:
132
+ if not isinstance(entry, str):
133
+ msg = (
134
+ f"{root_pyproject}: [tool.nab.workspace].members entries must be"
135
+ f" strings, got {type(entry).__name__}: {entry!r}"
136
+ )
137
+ raise WorkspaceDiscoveryError(msg)
138
+ if any(ch in entry for ch in "*?["):
139
+ msg = (
140
+ f"{root_pyproject}: globs in [tool.nab.workspace].members are not"
141
+ f" supported in nab; list members literally."
142
+ f" Offending entry: {entry!r}"
143
+ )
144
+ raise WorkspaceDiscoveryError(msg)
145
+ member_dir = (root_pyproject.parent / entry).resolve()
146
+ member_pyproject = member_dir / "pyproject.toml"
147
+ if not member_pyproject.is_file():
148
+ msg = (
149
+ f"{root_pyproject}: workspace member {entry!r} has no"
150
+ f" pyproject.toml at {member_pyproject}"
151
+ )
152
+ raise WorkspaceDiscoveryError(msg)
153
+ with member_pyproject.open("rb") as f:
154
+ member_data = tomli.load(f)
155
+ name = member_data.get("project", {}).get("name")
156
+ if not isinstance(name, str) or not name:
157
+ msg = (
158
+ f"{member_pyproject}: workspace member must declare"
159
+ f" [project].name (got {name!r})"
160
+ )
161
+ raise WorkspaceDiscoveryError(msg)
162
+ canonical = canonicalize_name(name)
163
+ if canonical in seen:
164
+ msg = (
165
+ f"{root_pyproject}: workspace members declare duplicate"
166
+ f" canonical name {canonical!r} via entries {seen[canonical]!r}"
167
+ f" and {entry!r}"
168
+ )
169
+ raise WorkspaceDiscoveryError(msg)
170
+ seen[canonical] = entry
171
+ sources.append(LocalSource(name=name, path=str(member_dir)))
172
+ return tuple(sources)
173
+
174
+
175
+ def merge_workspace_local_sources(
176
+ explicit: Iterable[LocalSource],
177
+ discovered: Iterable[LocalSource],
178
+ ) -> tuple[LocalSource, ...]:
179
+ """Combine explicit and workspace-discovered local sources.
180
+
181
+ Explicit ``[[tool.nab.local-sources]]`` entries always win. When a
182
+ discovered member shares a canonical name with an explicit entry,
183
+ the discovered entry is dropped and one INFO log line records the
184
+ shadow so the user can audit what was overridden. The order is
185
+ explicit entries first, then unshadowed discovered entries in the
186
+ order they were declared.
187
+ """
188
+ explicit_tuple = tuple(explicit)
189
+ explicit_names = {canonicalize_name(s.name) for s in explicit_tuple}
190
+ out = list(explicit_tuple)
191
+ for src in discovered:
192
+ if canonicalize_name(src.name) in explicit_names:
193
+ logger.info(
194
+ "workspace member %r at %s shadowed by explicit"
195
+ " [[tool.nab.local-sources]]",
196
+ src.name,
197
+ src.path,
198
+ )
199
+ continue
200
+ out.append(src)
201
+ return tuple(out)
202
+
203
+
204
+ def auto_promote_build_policy_for_workspace(current: BuildPolicy) -> BuildPolicy:
205
+ """Floor ``current`` at :attr:`BuildPolicy.BUILD_LOCAL`.
206
+
207
+ Workspace members frequently use ``dynamic = ["version"]`` (hatch's
208
+ pattern) and other dynamic fields, which require the local backend
209
+ path. The user's setting wins when it is already at least as
210
+ permissive as :attr:`BuildPolicy.BUILD_LOCAL`.
211
+ """
212
+ if _PERMISSIVENESS[current] < _PERMISSIVENESS[BuildPolicy.BUILD_LOCAL]:
213
+ return BuildPolicy.BUILD_LOCAL
214
+ return current
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: nab-python
3
+ Version: 0.0.1
4
+ Summary: Index-backed provider, lockfile emitter, and downloader for nab
5
+ Project-URL: Homepage, https://github.com/notatallshaw/nab
6
+ Project-URL: Documentation, https://nab.readthedocs.io/
7
+ Project-URL: Issues, https://github.com/notatallshaw/nab/issues
8
+ Project-URL: Source, https://github.com/notatallshaw/nab
9
+ Project-URL: Changelog, https://github.com/notatallshaw/nab/blob/main/CHANGELOG.md
10
+ Author-email: Damian Shaw <damian.peter.shaw@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: build>=1.2
22
+ Requires-Dist: installer>=0.7
23
+ Requires-Dist: nab-index==0.0.1
24
+ Requires-Dist: nab-resolver==0.0.1
25
+ Requires-Dist: pyproject-hooks>=1.2
26
+ Requires-Dist: tomli-w>=1.2
27
+ Requires-Dist: tomli>=2.0
28
+ Requires-Dist: typing-extensions>=4.6
29
+ Description-Content-Type: text/markdown
30
+
31
+ # nab-python
32
+
33
+ The Python package index provider that drives [`nab-resolver`](https://pypi.org/project/nab-resolver/)
34
+ for Python packages.
35
+
36
+ It owns all the mechanics of how the resolver needs to interact with standards
37
+ based Python package indexes and packages.
38
+
39
+ It implements build and distribution policies to allow the user to control
40
+ resolver and install behavior.
41
+
42
+ ## When to use it
43
+
44
+ Use `nab-python` if you need to embed Python package resolution in
45
+ another tool and want the resolver, provider, and lockfile emitter
46
+ without the CLI.
47
+
48
+ The API is currently under rapid experimentation, use exact version
49
+ pinning.
@@ -0,0 +1,71 @@
1
+ nab_python/__init__.py,sha256=tKRFGIR13xvNEftDBxxdczeKtcrvaqcOPiYbmdyFOOs,62
2
+ nab_python/_packaging_provider.py,sha256=-dxbaed8UoPtqxBnrOqxw2N_jPzHaQEMT1RJOoMHdRI,3435
3
+ nab_python/_vcs_admission.py,sha256=RSFnuZZW_-amCcfh3ERrCx_bBsLLuBcFX-Gt9dVy3fY,7021
4
+ nab_python/build_backend.py,sha256=T-KBp-VulIQATUjYvTt_hJronFceDSjvB-wZ3YHWPpw,6519
5
+ nab_python/config.py,sha256=UuLxZ3pn4pEQBKBRkMiWNV53xu-iswG81lQ6-mpdmLc,28988
6
+ nab_python/download.py,sha256=-wJGvpWqHSuQ2nK5cX-imC4pe6ypQDMYr8DLZy9ZauM,5398
7
+ nab_python/fetch.py,sha256=jB5G-7glsn_DycdrjFz0fek-iCWiBJYsN5KRvq6-3XQ,31760
8
+ nab_python/lockfile.py,sha256=OmO-owxmyWPUzwAgXhMgGphIv0hj1TeWW-hdF1sGqCs,7271
9
+ nab_python/metadata.py,sha256=hRVP7xt6CWl2h-Sxq1F1BSilOWj4yg0AxEnPyuRWPlw,4868
10
+ nab_python/provider.py,sha256=j9dutTZnuMMG8EFEYfUmXTiKb1Ge4FV7_sY-ET1gJKw,51256
11
+ nab_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ nab_python/requirements_file.py,sha256=t_7q1D-MUS_fH5-BLrXs4hB2txO5Iv-_b8vzmtINMsI,6327
13
+ nab_python/resolve.py,sha256=X_8nBI14ECNfCh3bkQyFMNEfSpv7Jfdfz3ITyoA5QdY,18275
14
+ nab_python/workspace.py,sha256=fKe4gyq09WkE1lSEXzkgYXeUKXcoW18SXOlYbVzY2hE,8006
15
+ nab_python/_build/__init__.py,sha256=7qW6Gk8Tr8fc3xon-yZ1zW5Z1oUU-LPRpgyucso1GNc,61
16
+ nab_python/_build/env.py,sha256=WxXMitoU2m526WFEi5NpNRgLOZkrrXuinan-uKY2uqQ,13790
17
+ nab_python/_build/errors.py,sha256=1--kmqmqsOYg7JOgYWysAiLhGJci8Fi929NvPT1ssXw,501
18
+ nab_python/_build/runner.py,sha256=VzCYGi6pIgd3d_06r5KXa0Ra5KXz-IPOrNkzCBynTcA,9356
19
+ nab_python/_lockfile/__init__.py,sha256=gfbgTLr4C66nEZeaI5G5DGGo6_s_qrB2uWAo3DjXC7Q,59
20
+ nab_python/_lockfile/builder.py,sha256=1YDp8yGXB1b-BrQ9NdoZYgjkEqs-qkQl_dp49BjhA8o,11531
21
+ nab_python/_lockfile/disjointness.py,sha256=fOg6Fdyk_ZuIlmqqzh3XjrRvLYPnJ2IhzJBboRln6b4,8495
22
+ nab_python/_lockfile/pylock.py,sha256=QKkbSz-Q5gJx-gl8hazhQetppPL-LRTaifFM9DtV5VU,10918
23
+ nab_python/_lockfile/requirements.py,sha256=etDNIM-2CIq3eWF5BMY1Uafb9V5DHEKoSSfmEZrBff4,4410
24
+ nab_python/_provider/__init__.py,sha256=FJwoSdUfrfhhXfaeJAIXQEWzY6KV4hKFNnvRkQVccfA,59
25
+ nab_python/_provider/build_remote.py,sha256=nqQ4rzXNkWxNouAyhhDC-rPZmtSsbPOaREoAPYm4Oz4,3411
26
+ nab_python/_provider/extras.py,sha256=SjJY33eXdF8MJ1KfIc2Hio3jxbpwaAwh7Rn4WZCEjMk,8293
27
+ nab_python/_provider/listing.py,sha256=SWYyPyjhIOTLbKi0B2jYxNC3KVtq5-3XFct_DHtp0gY,16512
28
+ nab_python/_provider/lookahead.py,sha256=xgX0f4h9dt2rdXZ_HmQmzXKyac2H4pDoaRMpVUqFotQ,5895
29
+ nab_python/_provider/metadata_resolver.py,sha256=rBWyDlq61DasSMkKp0ilh_S8VBNOJ63rUO63eG6T_Qc,16727
30
+ nab_python/_provider/priority.py,sha256=mYJHoMsoi71qTycyloWMmQ8ey00uNgBdIIkfXuar9yM,5975
31
+ nab_python/_provider/sources.py,sha256=qWnS13DKWtXLqCh9dJkN453fjhmO1o9IUurPIB0pu6w,7382
32
+ nab_python/_testing/__init__.py,sha256=WiMwVVwq6kg2mHFeM7JNA4-2tiXWVOPXq6YJd49vJQ0,44
33
+ nab_python/_testing/coordinator_fake.py,sha256=C2A70zT311tF4Lkj2UO8YYhHeiAYLMtOgm7HUMToWxM,8759
34
+ nab_python/_vendor/__init__.py,sha256=lSo_j79lhUNAEUBUs6RK5ZGyKM7NmjUhAXRmdGpRKmw,198
35
+ nab_python/_vendor/packaging/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
36
+ nab_python/_vendor/packaging/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
37
+ nab_python/_vendor/packaging/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
38
+ nab_python/_vendor/packaging/PROVENANCE.md,sha256=HZFDXXqPVjFCIxAK2QtT16jS-STyEOBulxGbhLokR10,2725
39
+ nab_python/_vendor/packaging/__init__.py,sha256=vwGIetD0qfqeO5zUYB3ECnspcLXICM3lnmTEqh55fJE,499
40
+ nab_python/_vendor/packaging/_elffile.py,sha256=-sKkptYqzYw2-x3QByJa5mB4rfPWu1pxkZHRx1WAFCY,3211
41
+ nab_python/_vendor/packaging/_manylinux.py,sha256=iF4g3wPHrxB7L7801xDd7Y9t8WvUnFPlhQXNT-GtV1I,9624
42
+ nab_python/_vendor/packaging/_musllinux.py,sha256=f12WeQCVwYA_yEcVgaYrrnXzp3meJTHD5LS_aUDn4us,2772
43
+ nab_python/_vendor/packaging/_parser.py,sha256=FSWddS3aQIyT4aMCTt5Wvs-O-iqVNvRFZSw7Q1FetLI,11712
44
+ nab_python/_vendor/packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
45
+ nab_python/_vendor/packaging/_tokenizer.py,sha256=VE_Lkm74DdF4as52JUfMRwKN81Lu_zSit6THmN2Ffq8,5456
46
+ nab_python/_vendor/packaging/dependency_groups.py,sha256=XZIAVFK9uHG4RCGprmJn3VInUWMesxha_kytJuMO9eY,10218
47
+ nab_python/_vendor/packaging/direct_url.py,sha256=eKmbDiPP1sLV4Mj_kCSZqqknrIyVO9Sr7JpF8KCjp4U,10917
48
+ nab_python/_vendor/packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
49
+ nab_python/_vendor/packaging/markers.py,sha256=bve3TML_xTL-QNpNWHrLiuTrDWmLROFUYjiC3cQKtoU,17470
50
+ nab_python/_vendor/packaging/metadata.py,sha256=crAh0E3GVGVqPlu6EdRFsaG-Y6UYznTUqjuGKRGPv6c,38770
51
+ nab_python/_vendor/packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ nab_python/_vendor/packaging/pylock.py,sha256=o3eyzUZ6DkKU2S-H2IZKZKTD4yCd0Gtw2uq_6hVjNBQ,33967
53
+ nab_python/_vendor/packaging/ranges.py,sha256=wgoGlYIBARnpGhYXJBxyPCrUDVk3Nv0Uuz4qhaeSwWU,67300
54
+ nab_python/_vendor/packaging/requirements.py,sha256=ijGq_2NxRpeOIto1z_RRMR8ZIvJoKZicFKxN8H4krlw,4448
55
+ nab_python/_vendor/packaging/specifiers.py,sha256=yDSDr4prgiIkMzWkHgSGFWjBeNrMpZaTIecCXQk7HA4,41156
56
+ nab_python/_vendor/packaging/tags.py,sha256=xegoSm6VqawLUlgIAB0WQYtKHAySYwxABTbOcBiVw_0,34234
57
+ nab_python/_vendor/packaging/utils.py,sha256=2J6uAgU8vJDIY5Lvqj5lO54Qc2yxhkpgIdSqjTgtYho,9841
58
+ nab_python/_vendor/packaging/version.py,sha256=daDMk4YRlKNgSiQkkIlxfmpsj_qGiWhVghgMiGUyc2M,38358
59
+ nab_python/_vendor/packaging/licenses/__init__.py,sha256=_Jx0XRiD_58palsWnyLrLuh59ZpGCPIPXLKdZo9OJvQ,7293
60
+ nab_python/_vendor/packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
61
+ nab_python/universal/__init__.py,sha256=eNFMbsK-jBNy1J7wAE702vI-LVXF_aaQQvX7G02ImUw,48
62
+ nab_python/universal/matrix.py,sha256=QOrU5oM3XhrWKlYRuwjdYau9o6DOhsngmr6VN8anJ3s,8215
63
+ nab_python/universal/provider.py,sha256=IrlMhoaCtDhjLyX7eu55Mq_lmsAEQhjzs3QLFUbC-lI,9238
64
+ nab_python/universal/reresolve.py,sha256=GFpkpR897XD6XLBISMNlNIqSHPeUZ7SjpL0USWUDKag,11548
65
+ nab_python/universal/resolve.py,sha256=zGhS3CPGINr1pZ3DBo6cZ4fj8SFmHOOOqzo-YVyuUug,18526
66
+ nab_python/universal/validate.py,sha256=t0lPQieaITnQdFYMqXB4o7LDrnL-K2EhuMSso1tlzo0,16269
67
+ nab_python/universal/wheel_selection.py,sha256=0qkwb3XgZdqOwVneO7Rfg7Rg02sNo6sUp9L1KszVHuc,12001
68
+ nab_python-0.0.1.dist-info/METADATA,sha256=ywrCnKV7Vcjv-X-SIxmnavN5pBLwaHSocrChbTst5po,1804
69
+ nab_python-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
70
+ nab_python-0.0.1.dist-info/licenses/LICENSE,sha256=oec5WE-g9eYDBVwbDbyKHR7_zK67vUXRoikkrsZRuJ8,1068
71
+ nab_python-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any