nab-python 0.0.1__tar.gz

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-0.0.1/LICENSE +21 -0
  2. nab_python-0.0.1/PKG-INFO +49 -0
  3. nab_python-0.0.1/README.md +19 -0
  4. nab_python-0.0.1/pyproject.toml +41 -0
  5. nab_python-0.0.1/src/nab_python/__init__.py +1 -0
  6. nab_python-0.0.1/src/nab_python/_build/__init__.py +1 -0
  7. nab_python-0.0.1/src/nab_python/_build/env.py +364 -0
  8. nab_python-0.0.1/src/nab_python/_build/errors.py +17 -0
  9. nab_python-0.0.1/src/nab_python/_build/runner.py +254 -0
  10. nab_python-0.0.1/src/nab_python/_lockfile/__init__.py +1 -0
  11. nab_python-0.0.1/src/nab_python/_lockfile/builder.py +339 -0
  12. nab_python-0.0.1/src/nab_python/_lockfile/disjointness.py +207 -0
  13. nab_python-0.0.1/src/nab_python/_lockfile/pylock.py +323 -0
  14. nab_python-0.0.1/src/nab_python/_lockfile/requirements.py +121 -0
  15. nab_python-0.0.1/src/nab_python/_packaging_provider.py +98 -0
  16. nab_python-0.0.1/src/nab_python/_provider/__init__.py +1 -0
  17. nab_python-0.0.1/src/nab_python/_provider/build_remote.py +95 -0
  18. nab_python-0.0.1/src/nab_python/_provider/extras.py +231 -0
  19. nab_python-0.0.1/src/nab_python/_provider/listing.py +442 -0
  20. nab_python-0.0.1/src/nab_python/_provider/lookahead.py +156 -0
  21. nab_python-0.0.1/src/nab_python/_provider/metadata_resolver.py +450 -0
  22. nab_python-0.0.1/src/nab_python/_provider/priority.py +174 -0
  23. nab_python-0.0.1/src/nab_python/_provider/sources.py +215 -0
  24. nab_python-0.0.1/src/nab_python/_testing/__init__.py +1 -0
  25. nab_python-0.0.1/src/nab_python/_testing/coordinator_fake.py +240 -0
  26. nab_python-0.0.1/src/nab_python/_vcs_admission.py +209 -0
  27. nab_python-0.0.1/src/nab_python/_vendor/__init__.py +6 -0
  28. nab_python-0.0.1/src/nab_python/_vendor/packaging/LICENSE +3 -0
  29. nab_python-0.0.1/src/nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  30. nab_python-0.0.1/src/nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  31. nab_python-0.0.1/src/nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  32. nab_python-0.0.1/src/nab_python/_vendor/packaging/__init__.py +15 -0
  33. nab_python-0.0.1/src/nab_python/_vendor/packaging/_elffile.py +108 -0
  34. nab_python-0.0.1/src/nab_python/_vendor/packaging/_manylinux.py +265 -0
  35. nab_python-0.0.1/src/nab_python/_vendor/packaging/_musllinux.py +88 -0
  36. nab_python-0.0.1/src/nab_python/_vendor/packaging/_parser.py +394 -0
  37. nab_python-0.0.1/src/nab_python/_vendor/packaging/_structures.py +33 -0
  38. nab_python-0.0.1/src/nab_python/_vendor/packaging/_tokenizer.py +196 -0
  39. nab_python-0.0.1/src/nab_python/_vendor/packaging/dependency_groups.py +302 -0
  40. nab_python-0.0.1/src/nab_python/_vendor/packaging/direct_url.py +325 -0
  41. nab_python-0.0.1/src/nab_python/_vendor/packaging/errors.py +94 -0
  42. nab_python-0.0.1/src/nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  43. nab_python-0.0.1/src/nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  44. nab_python-0.0.1/src/nab_python/_vendor/packaging/markers.py +506 -0
  45. nab_python-0.0.1/src/nab_python/_vendor/packaging/metadata.py +964 -0
  46. nab_python-0.0.1/src/nab_python/_vendor/packaging/py.typed +0 -0
  47. nab_python-0.0.1/src/nab_python/_vendor/packaging/pylock.py +910 -0
  48. nab_python-0.0.1/src/nab_python/_vendor/packaging/ranges.py +1803 -0
  49. nab_python-0.0.1/src/nab_python/_vendor/packaging/requirements.py +132 -0
  50. nab_python-0.0.1/src/nab_python/_vendor/packaging/specifiers.py +1141 -0
  51. nab_python-0.0.1/src/nab_python/_vendor/packaging/tags.py +929 -0
  52. nab_python-0.0.1/src/nab_python/_vendor/packaging/utils.py +296 -0
  53. nab_python-0.0.1/src/nab_python/_vendor/packaging/version.py +1230 -0
  54. nab_python-0.0.1/src/nab_python/build_backend.py +184 -0
  55. nab_python-0.0.1/src/nab_python/config.py +805 -0
  56. nab_python-0.0.1/src/nab_python/download.py +170 -0
  57. nab_python-0.0.1/src/nab_python/fetch.py +827 -0
  58. nab_python-0.0.1/src/nab_python/lockfile.py +238 -0
  59. nab_python-0.0.1/src/nab_python/metadata.py +145 -0
  60. nab_python-0.0.1/src/nab_python/provider.py +1235 -0
  61. nab_python-0.0.1/src/nab_python/py.typed +0 -0
  62. nab_python-0.0.1/src/nab_python/requirements_file.py +180 -0
  63. nab_python-0.0.1/src/nab_python/resolve.py +497 -0
  64. nab_python-0.0.1/src/nab_python/universal/__init__.py +1 -0
  65. nab_python-0.0.1/src/nab_python/universal/matrix.py +235 -0
  66. nab_python-0.0.1/src/nab_python/universal/provider.py +214 -0
  67. nab_python-0.0.1/src/nab_python/universal/reresolve.py +310 -0
  68. nab_python-0.0.1/src/nab_python/universal/resolve.py +508 -0
  69. nab_python-0.0.1/src/nab_python/universal/validate.py +439 -0
  70. nab_python-0.0.1/src/nab_python/universal/wheel_selection.py +327 -0
  71. nab_python-0.0.1/src/nab_python/workspace.py +214 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Damian Shaw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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,19 @@
1
+ # nab-python
2
+
3
+ The Python package index provider that drives [`nab-resolver`](https://pypi.org/project/nab-resolver/)
4
+ for Python packages.
5
+
6
+ It owns all the mechanics of how the resolver needs to interact with standards
7
+ based Python package indexes and packages.
8
+
9
+ It implements build and distribution policies to allow the user to control
10
+ resolver and install behavior.
11
+
12
+ ## When to use it
13
+
14
+ Use `nab-python` if you need to embed Python package resolution in
15
+ another tool and want the resolver, provider, and lockfile emitter
16
+ without the CLI.
17
+
18
+ The API is currently under rapid experimentation, use exact version
19
+ pinning.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "nab-python"
3
+ version = "0.0.1"
4
+ description = "Index-backed provider, lockfile emitter, and downloader for nab"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Damian Shaw", email = "damian.peter.shaw@gmail.com" },
10
+ ]
11
+ requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "nab-resolver==0.0.1",
23
+ "nab-index==0.0.1",
24
+ "tomli>=2.0",
25
+ "tomli_w>=1.2",
26
+ "build>=1.2",
27
+ "installer>=0.7",
28
+ "pyproject_hooks>=1.2",
29
+ "typing_extensions>=4.6",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/notatallshaw/nab"
34
+ Documentation = "https://nab.readthedocs.io/"
35
+ Issues = "https://github.com/notatallshaw/nab/issues"
36
+ Source = "https://github.com/notatallshaw/nab"
37
+ Changelog = "https://github.com/notatallshaw/nab/blob/main/CHANGELOG.md"
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
@@ -0,0 +1 @@
1
+ """Python package resolver built on the nab resolver core."""
@@ -0,0 +1 @@
1
+ """PEP 517 build-backend invocation for dynamic metadata."""
@@ -0,0 +1,364 @@
1
+ """Isolated build env that uses nab itself plus the PyPA ``installer``.
2
+
3
+ ``NabBuildEnv`` implements ``build.env.IsolatedEnv`` so it slots into
4
+ ``build.ProjectBuilder.from_isolated_env``. The pieces:
5
+
6
+ * ``venv.EnvBuilder`` (stdlib) creates an empty interpreter at a temp
7
+ path, ``with_pip=False``; nab does not need pip in there.
8
+ * nab's own resolver picks versions for ``[build-system].requires``
9
+ using the same indexes / ``exclude-newer`` window as the outer
10
+ resolve.
11
+ * ``download_lock`` from :mod:`nab_python.download` fetches the
12
+ resolved wheels into a temp directory.
13
+ * :func:`installer.install` writes each wheel into the venv via
14
+ ``installer.SchemeDictionaryDestination``, configured from the
15
+ venv's own ``sysconfig.get_paths()``.
16
+
17
+ The env is a context manager. Entering it builds the venv and
18
+ installs the requirements; exiting removes the temp tree. The
19
+ ``python_executable`` and ``make_extra_environ`` properties come
20
+ from ``build.env.IsolatedEnv``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import logging
27
+ import os
28
+ import subprocess
29
+ import sys
30
+ import tempfile
31
+ import venv
32
+ from pathlib import Path
33
+ from typing import TYPE_CHECKING
34
+
35
+ from installer import install as installer_install
36
+ from installer.destinations import SchemeDictionaryDestination
37
+ from installer.sources import WheelFile
38
+ from nab_index.urllib3_async_transport import Urllib3AsyncTransport
39
+
40
+ from ..config import NabProjectConfig
41
+ from ..download import download_lock
42
+
43
+ if TYPE_CHECKING:
44
+ from collections.abc import Callable, Mapping
45
+ from typing_extensions import Self
46
+
47
+ from installer.records import RecordEntry
48
+ from installer.utils import Scheme
49
+ from nab_index.transport import AsyncHttpTransport
50
+
51
+ __all__ = [
52
+ "NabBuildEnv",
53
+ ]
54
+
55
+
56
+ class _FastSchemeDictionaryDestination(SchemeDictionaryDestination):
57
+ """``SchemeDictionaryDestination`` that skips bytecode-compile work.
58
+
59
+ The stock ``_compile_bytecode`` always resolves the target file's
60
+ real path before checking ``bytecode_optimization_levels``; with
61
+ an empty levels tuple, the loop body never runs but the resolve
62
+ still hits the filesystem twice per record. Skipping the early
63
+ work when the levels tuple is empty is safe because the body
64
+ would have been a no-op.
65
+ """
66
+
67
+ def _compile_bytecode(self, scheme: Scheme, record: RecordEntry) -> None:
68
+ if not self.bytecode_optimization_levels:
69
+ return
70
+ super()._compile_bytecode(scheme, record)
71
+
72
+
73
+ logger = logging.getLogger(__name__)
74
+
75
+
76
+ _LAUNCHER_KIND = (
77
+ "win-amd64"
78
+ if sys.platform == "win32" and sys.maxsize > 2**32
79
+ else "win-ia32"
80
+ if sys.platform == "win32"
81
+ else "posix"
82
+ )
83
+
84
+
85
+ class BuildEnvError(Exception):
86
+ """The build env could not be set up (resolve, download, or install)."""
87
+
88
+
89
+ class NabBuildEnv:
90
+ """An isolated PEP 518 build environment driven by nab.
91
+
92
+ Implements ``build.env.IsolatedEnv`` so it can be passed to
93
+ ``build.ProjectBuilder.from_isolated_env``. The runtime cost
94
+ is one venv creation, one inner resolve over
95
+ ``[build-system].requires``, one wheel download per dep, and
96
+ one ``installer.install`` per wheel.
97
+
98
+ ``requires`` is the PEP 508 string list from
99
+ ``[build-system].requires``. ``config`` carries the indexes,
100
+ ``exclude-newer`` window and other nab inputs from the outer
101
+ resolve; it is pruned (no local sources, no workspace, no
102
+ marker overlay) before the inner resolve so the build env is
103
+ computed against PyPI alone.
104
+
105
+ Construction is cheap; the work happens in ``__enter__``.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ requires: list[str],
111
+ *,
112
+ config: NabProjectConfig,
113
+ python_version: str | None = None,
114
+ transport_factory: Callable[[], AsyncHttpTransport] = Urllib3AsyncTransport,
115
+ ) -> None:
116
+ """Capture inputs; the venv and inner resolve happen in __enter__."""
117
+ self._requires = list(requires)
118
+ self._config = config
119
+ self._python_version = python_version
120
+ self._transport = transport_factory()
121
+
122
+ self._tmpdir: tempfile.TemporaryDirectory[str] | None = None
123
+ self._venv_path: Path | None = None
124
+ self._python_executable: Path | None = None
125
+ self._scripts_dir: Path | None = None
126
+
127
+ def __enter__(self) -> Self:
128
+ """Build the venv, run the inner resolve, install build requirements."""
129
+ self._tmpdir = tempfile.TemporaryDirectory(prefix="nab-build-env-")
130
+ try:
131
+ self._provision(Path(self._tmpdir.name))
132
+ except BaseException:
133
+ self._tmpdir.cleanup()
134
+ self._tmpdir = None
135
+ raise
136
+ return self
137
+
138
+ def _provision(self, root: Path) -> None:
139
+ """Lay out the venv, install build requirements, populate paths."""
140
+ self._venv_path = root / "venv"
141
+ wheel_dir = root / "wheels"
142
+ wheel_dir.mkdir()
143
+
144
+ logger.debug("creating build venv at %s", self._venv_path)
145
+ builder = venv.EnvBuilder(
146
+ with_pip=False, symlinks=_supports_symlinks(), clear=False
147
+ )
148
+ builder.create(self._venv_path)
149
+
150
+ self._python_executable = _venv_python(self._venv_path)
151
+ self._scripts_dir = self._python_executable.parent
152
+
153
+ scheme_dict = _venv_scheme_paths(self._python_executable)
154
+
155
+ if not self._requires:
156
+ return
157
+ wheel_paths = self._resolve_and_download(wheel_dir)
158
+ destination = _FastSchemeDictionaryDestination(
159
+ scheme_dict=scheme_dict,
160
+ interpreter=str(self._python_executable),
161
+ script_kind=_LAUNCHER_KIND,
162
+ bytecode_optimization_levels=(),
163
+ overwrite_existing=True,
164
+ )
165
+ for wheel_path in wheel_paths:
166
+ logger.debug("installing %s", wheel_path.name)
167
+ with WheelFile.open(wheel_path) as source:
168
+ installer_install(
169
+ source=source,
170
+ destination=destination,
171
+ additional_metadata={"INSTALLER": b"nab\n"},
172
+ )
173
+
174
+ def __exit__(self, *args: object) -> None:
175
+ """Remove the temp tree that holds the venv and downloaded wheels."""
176
+ if self._tmpdir is not None:
177
+ self._tmpdir.cleanup()
178
+ self._tmpdir = None
179
+
180
+ @property
181
+ def python_executable(self) -> str:
182
+ """Path to the venv's interpreter (str, per ``IsolatedEnv``)."""
183
+ if self._python_executable is None:
184
+ msg = "NabBuildEnv used outside its context-manager scope"
185
+ raise BuildEnvError(msg)
186
+ return str(self._python_executable)
187
+
188
+ def make_extra_environ(self) -> Mapping[str, str]:
189
+ """PATH / PYTHONPATH overrides for the build subprocess.
190
+
191
+ Prepends the venv's scripts dir so build backends find their
192
+ installed entry points; clears ``PYTHONPATH`` so the host's
193
+ ``sys.path`` does not leak in (matches the convention used by
194
+ ``build.env.DefaultIsolatedEnv``).
195
+ """
196
+ if self._scripts_dir is None:
197
+ msg = "NabBuildEnv used outside its context-manager scope"
198
+ raise BuildEnvError(msg)
199
+ host_path = os.environ.get("PATH")
200
+ new_path = (
201
+ f"{self._scripts_dir}{os.pathsep}{host_path}"
202
+ if host_path
203
+ else str(self._scripts_dir)
204
+ )
205
+ return {"PATH": new_path, "PYTHONPATH": ""}
206
+
207
+ def install(self, requirements: list[str]) -> None:
208
+ """Install additional requirements into the live env.
209
+
210
+ Used for ``get_requires_for_build_wheel`` follow-up requests
211
+ (the backend asks for additional deps after the env is
212
+ already up). Same code path as the constructor's install,
213
+ targeting the same venv.
214
+ """
215
+ if self._venv_path is None or self._python_executable is None:
216
+ msg = "NabBuildEnv used outside its context-manager scope"
217
+ raise BuildEnvError(msg)
218
+ if not requirements:
219
+ return
220
+ wheel_dir = self._venv_path.parent / "wheels"
221
+ # Append a fresh subdir so a re-install does not re-download
222
+ # the same wheel into the same path under a different version.
223
+ sub = wheel_dir / f"_extra_{len(list(wheel_dir.iterdir()))}"
224
+ sub.mkdir(parents=True, exist_ok=True)
225
+ wheel_paths = self._resolve_and_download(sub, extra=requirements)
226
+ scheme_dict = _venv_scheme_paths(self._python_executable)
227
+ destination = SchemeDictionaryDestination(
228
+ scheme_dict=scheme_dict,
229
+ interpreter=str(self._python_executable),
230
+ script_kind=_LAUNCHER_KIND,
231
+ bytecode_optimization_levels=(),
232
+ overwrite_existing=True,
233
+ )
234
+ for wheel_path in wheel_paths:
235
+ with WheelFile.open(wheel_path) as source:
236
+ installer_install(
237
+ source=source,
238
+ destination=destination,
239
+ additional_metadata={"INSTALLER": b"nab\n"},
240
+ )
241
+
242
+ def _resolve_and_download(
243
+ self,
244
+ wheel_dir: Path,
245
+ *,
246
+ extra: list[str] | None = None,
247
+ ) -> list[Path]:
248
+ """Resolve ``requires`` (+ ``extra``) and write wheels under ``wheel_dir``.
249
+
250
+ The inner resolve runs against a synthetic pyproject so it
251
+ can reuse :func:`nab_python.resolve.resolve_pyproject` and
252
+ :func:`nab_python.download.download_lock` end-to-end. No
253
+ local sources / workspace / marker overlay; build deps
254
+ come from the configured indexes only.
255
+ """
256
+ # Late import: avoids a cycle through ``resolve.py`` which
257
+ # itself imports ``pypi.py`` which imports ``build_backend``
258
+ # which imports this module.
259
+ from ..resolve import resolve_pyproject
260
+
261
+ requires = list(self._requires)
262
+ if extra:
263
+ requires.extend(extra)
264
+
265
+ synthetic_dir = wheel_dir.parent / "_inner_project"
266
+ synthetic_dir.mkdir(parents=True, exist_ok=True)
267
+ synthetic = synthetic_dir / "pyproject.toml"
268
+ synthetic.write_text(_render_synthetic_pyproject(requires), encoding="utf-8")
269
+
270
+ inner_config = NabProjectConfig(
271
+ indexes=self._config.indexes,
272
+ index_overrides=self._config.index_overrides,
273
+ uploaded_prior_to=self._config.uploaded_prior_to,
274
+ uploaded_prior_to_overrides=self._config.uploaded_prior_to_overrides,
275
+ )
276
+ result = resolve_pyproject(
277
+ synthetic,
278
+ self._transport,
279
+ config=inner_config,
280
+ python_version=self._python_version,
281
+ )
282
+
283
+ # Reject sdist-only pins early: build deps that ship only an
284
+ # sdist trigger a recursive backend invocation that this
285
+ # builder does not handle. Most build tools (hatchling,
286
+ # setuptools, flit, pdm-backend) publish wheels.
287
+ from ..lockfile import IndexPin
288
+
289
+ sdist_only: list[str] = []
290
+ for canonical, pin in result.lock_input.pins.items():
291
+ if isinstance(pin, IndexPin) and not pin.wheels:
292
+ sdist_only.append(f"{canonical}=={pin.version}")
293
+ if sdist_only:
294
+ msg = (
295
+ "build env requires sdist-only packages which nab cannot"
296
+ " install without recursing through the build path: "
297
+ + ", ".join(sdist_only)
298
+ )
299
+ raise BuildEnvError(msg)
300
+
301
+ download_result = download_lock(result.lock_input, self._transport, wheel_dir)
302
+ # Both wheels and sdists are downloaded; only wheels feed
303
+ # ``installer.install``. The sdists are inert clutter under
304
+ # the temp dir, cleaned up with the env.
305
+ all_paths = list(download_result.written) + list(download_result.skipped)
306
+ return [p for p in all_paths if p.suffix == ".whl"]
307
+
308
+
309
+ def _venv_python(venv_path: Path) -> Path:
310
+ r"""Return the venv interpreter path (``Scripts\python.exe`` on Windows)."""
311
+ if sys.platform == "win32":
312
+ return venv_path / "Scripts" / "python.exe" # pragma: no cover
313
+ return venv_path / "bin" / "python"
314
+
315
+
316
+ def _venv_scheme_paths(python_executable: Path) -> dict[str, str]:
317
+ """Ask the venv's interpreter for its own ``sysconfig.get_paths()``.
318
+
319
+ Subprocessing the venv guarantees the returned paths reflect the
320
+ venv's layout (``site-packages`` under the venv root, scripts in
321
+ its ``bin``/``Scripts`` dir, etc.) regardless of how nab itself
322
+ was installed. One subprocess per env construction; negligible.
323
+ """
324
+ result = subprocess.run( # noqa: S603 - controlled command, no shell
325
+ [
326
+ str(python_executable),
327
+ "-c",
328
+ "import json, sysconfig; print(json.dumps(sysconfig.get_paths()))",
329
+ ],
330
+ capture_output=True,
331
+ text=True,
332
+ check=True,
333
+ )
334
+ return dict(json.loads(result.stdout))
335
+
336
+
337
+ def _supports_symlinks() -> bool:
338
+ """``venv.EnvBuilder(symlinks=...)`` heuristic; avoids Windows traps."""
339
+ return sys.platform != "win32"
340
+
341
+
342
+ def _render_synthetic_pyproject(requires: list[str]) -> str:
343
+ """Render a tiny pyproject.toml whose deps are ``requires``.
344
+
345
+ Used as input to the inner resolve; the on-disk file is
346
+ discarded with the rest of the temp tree. The dummy name and
347
+ version make the inner resolver happy without depending on the
348
+ outer project's name/version.
349
+ """
350
+ deps_block = ",\n ".join(_toml_str(s) for s in requires)
351
+ if deps_block:
352
+ deps_block = f"\n {deps_block},\n"
353
+ return (
354
+ "[project]\n"
355
+ 'name = "_nab_build_env"\n'
356
+ 'version = "0.0.0"\n'
357
+ f"dependencies = [{deps_block}]\n"
358
+ )
359
+
360
+
361
+ def _toml_str(value: str) -> str:
362
+ """Single-line TOML string literal escape."""
363
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
364
+ return f'"{escaped}"'
@@ -0,0 +1,17 @@
1
+ """Errors shared between the build runner and its callers.
2
+
3
+ Lives in its own module so :mod:`nab_python.build_backend` can
4
+ import the exception without pulling in :mod:`build`,
5
+ :mod:`pyproject_hooks`, and the rest of the runner module at module
6
+ load time. The runner re-exports the class for back-compat.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__ = [
12
+ "BuildBackendError",
13
+ ]
14
+
15
+
16
+ class BuildBackendError(Exception):
17
+ """A build-backend operation failed or returned unparseable metadata."""