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.
- nab_python-0.0.1/LICENSE +21 -0
- nab_python-0.0.1/PKG-INFO +49 -0
- nab_python-0.0.1/README.md +19 -0
- nab_python-0.0.1/pyproject.toml +41 -0
- nab_python-0.0.1/src/nab_python/__init__.py +1 -0
- nab_python-0.0.1/src/nab_python/_build/__init__.py +1 -0
- nab_python-0.0.1/src/nab_python/_build/env.py +364 -0
- nab_python-0.0.1/src/nab_python/_build/errors.py +17 -0
- nab_python-0.0.1/src/nab_python/_build/runner.py +254 -0
- nab_python-0.0.1/src/nab_python/_lockfile/__init__.py +1 -0
- nab_python-0.0.1/src/nab_python/_lockfile/builder.py +339 -0
- nab_python-0.0.1/src/nab_python/_lockfile/disjointness.py +207 -0
- nab_python-0.0.1/src/nab_python/_lockfile/pylock.py +323 -0
- nab_python-0.0.1/src/nab_python/_lockfile/requirements.py +121 -0
- nab_python-0.0.1/src/nab_python/_packaging_provider.py +98 -0
- nab_python-0.0.1/src/nab_python/_provider/__init__.py +1 -0
- nab_python-0.0.1/src/nab_python/_provider/build_remote.py +95 -0
- nab_python-0.0.1/src/nab_python/_provider/extras.py +231 -0
- nab_python-0.0.1/src/nab_python/_provider/listing.py +442 -0
- nab_python-0.0.1/src/nab_python/_provider/lookahead.py +156 -0
- nab_python-0.0.1/src/nab_python/_provider/metadata_resolver.py +450 -0
- nab_python-0.0.1/src/nab_python/_provider/priority.py +174 -0
- nab_python-0.0.1/src/nab_python/_provider/sources.py +215 -0
- nab_python-0.0.1/src/nab_python/_testing/__init__.py +1 -0
- nab_python-0.0.1/src/nab_python/_testing/coordinator_fake.py +240 -0
- nab_python-0.0.1/src/nab_python/_vcs_admission.py +209 -0
- nab_python-0.0.1/src/nab_python/_vendor/__init__.py +6 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/LICENSE +3 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/LICENSE.BSD +23 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/PROVENANCE.md +73 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/__init__.py +15 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/_elffile.py +108 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/_manylinux.py +265 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/_musllinux.py +88 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/_parser.py +394 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/_structures.py +33 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/_tokenizer.py +196 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/dependency_groups.py +302 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/direct_url.py +325 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/errors.py +94 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/licenses/__init__.py +186 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/markers.py +506 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/metadata.py +964 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/py.typed +0 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/pylock.py +910 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/ranges.py +1803 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/requirements.py +132 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/specifiers.py +1141 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/tags.py +929 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/utils.py +296 -0
- nab_python-0.0.1/src/nab_python/_vendor/packaging/version.py +1230 -0
- nab_python-0.0.1/src/nab_python/build_backend.py +184 -0
- nab_python-0.0.1/src/nab_python/config.py +805 -0
- nab_python-0.0.1/src/nab_python/download.py +170 -0
- nab_python-0.0.1/src/nab_python/fetch.py +827 -0
- nab_python-0.0.1/src/nab_python/lockfile.py +238 -0
- nab_python-0.0.1/src/nab_python/metadata.py +145 -0
- nab_python-0.0.1/src/nab_python/provider.py +1235 -0
- nab_python-0.0.1/src/nab_python/py.typed +0 -0
- nab_python-0.0.1/src/nab_python/requirements_file.py +180 -0
- nab_python-0.0.1/src/nab_python/resolve.py +497 -0
- nab_python-0.0.1/src/nab_python/universal/__init__.py +1 -0
- nab_python-0.0.1/src/nab_python/universal/matrix.py +235 -0
- nab_python-0.0.1/src/nab_python/universal/provider.py +214 -0
- nab_python-0.0.1/src/nab_python/universal/reresolve.py +310 -0
- nab_python-0.0.1/src/nab_python/universal/resolve.py +508 -0
- nab_python-0.0.1/src/nab_python/universal/validate.py +439 -0
- nab_python-0.0.1/src/nab_python/universal/wheel_selection.py +327 -0
- nab_python-0.0.1/src/nab_python/workspace.py +214 -0
nab_python-0.0.1/LICENSE
ADDED
|
@@ -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."""
|