pyproject-api 1.11.6__tar.gz → 1.12.0__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.
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.github/workflows/release.yaml +2 -2
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/PKG-INFO +1 -1
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/_backend.py +25 -8
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/_frontend.py +32 -7
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/_version.py +2 -2
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/test_backend.py +60 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/test_frontend.py +120 -7
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.github/FUNDING.yaml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.github/SECURITY.md +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.github/dependabot.yaml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.github/release.yaml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.github/workflows/check.yaml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.gitignore +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.pre-commit-config.yaml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/.readthedocs.yml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/CODE_OF_CONDUCT.md +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/LICENSE +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/README.md +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/docs/api.rst +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/docs/conf.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/docs/index.rst +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/pyproject.toml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/__init__.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/__main__.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/_backend.pyi +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/_util.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/_via_fresh_subprocess.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/src/pyproject_api/py.typed +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/_build_sdist.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/demo_pkg_inline/build.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/demo_pkg_inline/pyproject.toml +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/test_frontend_setuptools.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/test_main.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/test_util.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tests/test_version.py +0 -0
- {pyproject_api-1.11.6 → pyproject_api-1.12.0}/tox.toml +0 -0
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
- name: Build package
|
|
28
28
|
run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist
|
|
29
29
|
- name: Store the distribution packages
|
|
30
|
-
uses: actions/upload-artifact@
|
|
30
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
31
31
|
with:
|
|
32
32
|
name: ${{ env.dists-artifact-name }}
|
|
33
33
|
path: dist/*
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
id-token: write
|
|
44
44
|
steps:
|
|
45
45
|
- name: Download all the dists
|
|
46
|
-
uses: actions/download-artifact@
|
|
46
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
47
47
|
with:
|
|
48
48
|
name: ${{ env.dists-artifact-name }}
|
|
49
49
|
path: dist/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyproject-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Summary: API to interact with the python pyproject.toml based projects
|
|
5
5
|
Project-URL: Changelog, https://github.com/tox-dev/pyproject-api/releases
|
|
6
6
|
Project-URL: Homepage, https://pyproject-api.readthedocs.io
|
|
@@ -23,7 +23,8 @@ class BackendProxy:
|
|
|
23
23
|
self.backend_object = backend_obj
|
|
24
24
|
backend = importlib.import_module(self.backend_module)
|
|
25
25
|
if self.backend_object:
|
|
26
|
-
|
|
26
|
+
for name in self.backend_object.split("."):
|
|
27
|
+
backend = getattr(backend, name)
|
|
27
28
|
self.backend = backend
|
|
28
29
|
|
|
29
30
|
def __call__(self, name, *args, **kwargs):
|
|
@@ -87,23 +88,39 @@ def run(argv): # ruff:ignore[complex-structure, too-many-branches, too-many-sta
|
|
|
87
88
|
flush()
|
|
88
89
|
else:
|
|
89
90
|
result = {}
|
|
91
|
+
response = None
|
|
90
92
|
try:
|
|
91
93
|
cmd = parsed_message["cmd"]
|
|
92
94
|
print("Backend: run command {} with args {}".format(cmd, parsed_message["kwargs"]))
|
|
93
95
|
result["return"] = backend_proxy(cmd, **parsed_message["kwargs"])
|
|
94
|
-
|
|
95
|
-
break
|
|
96
|
+
response = json.dumps(result)
|
|
96
97
|
except BaseException as exception: # ruff:ignore[blind-except]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
try:
|
|
99
|
+
result = {
|
|
100
|
+
"code": exception.code if isinstance(exception, SystemExit) else 1,
|
|
101
|
+
"exc_type": exception.__class__.__name__,
|
|
102
|
+
"exc_msg": str(exception),
|
|
103
|
+
}
|
|
104
|
+
response = json.dumps(result)
|
|
105
|
+
except BaseException: # ruff:ignore[blind-except]
|
|
106
|
+
result = {
|
|
107
|
+
"code": 1,
|
|
108
|
+
"exc_type": "RuntimeError",
|
|
109
|
+
"exc_msg": "Failed to serialize backend exception",
|
|
110
|
+
}
|
|
111
|
+
response = json.dumps(result)
|
|
100
112
|
if not isinstance(exception, MissingCommand): # for missing command do not print stack
|
|
101
113
|
traceback.print_exc()
|
|
114
|
+
else:
|
|
115
|
+
if cmd == "_exit":
|
|
116
|
+
break
|
|
102
117
|
finally:
|
|
103
118
|
try:
|
|
104
119
|
encoding = locale.getpreferredencoding(do_setlocale=False)
|
|
105
|
-
with open(
|
|
106
|
-
|
|
120
|
+
with open( # ruff:ignore[builtin-open, write-whole-file] # Python 2.7 compatible
|
|
121
|
+
result_file, "w", encoding=encoding
|
|
122
|
+
) as file_handler:
|
|
123
|
+
file_handler.write(response)
|
|
107
124
|
except Exception: # ruff:ignore[blind-except]
|
|
108
125
|
traceback.print_exc()
|
|
109
126
|
finally:
|
|
@@ -9,7 +9,7 @@ from contextlib import contextmanager
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
11
11
|
from time import sleep
|
|
12
|
-
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, NoReturn, TypedDict, cast
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Final, Literal, NamedTuple, NoReturn, TypedDict, cast
|
|
13
13
|
from zipfile import ZipFile
|
|
14
14
|
|
|
15
15
|
from packaging.requirements import Requirement
|
|
@@ -211,13 +211,16 @@ class Frontend(ABC):
|
|
|
211
211
|
|
|
212
212
|
:param folder: the python project folder
|
|
213
213
|
:return: the frontend creation args
|
|
214
|
-
:raises ValueError: if ``build-system`` is not a table,
|
|
214
|
+
:raises ValueError: if ``build-system`` is not a table, one of its keys has a type PEP-518 rules out, or a
|
|
215
|
+
``backend-path`` entry does not resolve inside the project (PEP-517 in-tree backends)
|
|
215
216
|
|
|
216
217
|
E.g., to create a frontend from a python project folder:
|
|
217
218
|
|
|
218
219
|
.. code:: python
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
from pyproject_api import SubprocessFrontend
|
|
222
|
+
|
|
223
|
+
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(project_folder)[:-1])
|
|
221
224
|
"""
|
|
222
225
|
folder = folder.absolute()
|
|
223
226
|
py_project_toml = folder / "pyproject.toml"
|
|
@@ -228,7 +231,9 @@ class Frontend(ABC):
|
|
|
228
231
|
msg = f"build-system must be a table, got {build_system!r}"
|
|
229
232
|
raise ValueError(msg)
|
|
230
233
|
if "backend-path" in build_system:
|
|
231
|
-
backend_paths: tuple[Path, ...] = tuple(
|
|
234
|
+
backend_paths: tuple[Path, ...] = tuple(
|
|
235
|
+
_in_tree_path(folder, entry) for entry in _str_list(build_system, "backend-path")
|
|
236
|
+
)
|
|
232
237
|
else:
|
|
233
238
|
backend_paths = ()
|
|
234
239
|
if "requires" in build_system:
|
|
@@ -343,9 +348,16 @@ class Frontend(ABC):
|
|
|
343
348
|
return MetadataForBuildWheelResult(metadata_directory / basename, out, err)
|
|
344
349
|
|
|
345
350
|
def _check_metadata_dir(self, metadata_directory: Path) -> None:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
351
|
+
metadata_path: Final[Path] = metadata_directory.resolve()
|
|
352
|
+
project_root: Final[Path] = self._root.resolve()
|
|
353
|
+
if metadata_path == project_root:
|
|
354
|
+
same_root_message: Final[str] = (
|
|
355
|
+
f"the project root and the metadata directory can't be the same {self._root}"
|
|
356
|
+
)
|
|
357
|
+
raise RuntimeError(same_root_message)
|
|
358
|
+
if metadata_path in project_root.parents:
|
|
359
|
+
ancestor_message: Final[str] = f"the metadata directory can't contain the project root {self._root}"
|
|
360
|
+
raise RuntimeError(ancestor_message)
|
|
349
361
|
if metadata_directory.exists(): # start with fresh
|
|
350
362
|
ensure_empty_dir(metadata_directory)
|
|
351
363
|
metadata_directory.mkdir(parents=True, exist_ok=True)
|
|
@@ -539,6 +551,19 @@ def _str_list(build_system: dict[str, Any], key: str) -> list[str]:
|
|
|
539
551
|
return value
|
|
540
552
|
|
|
541
553
|
|
|
554
|
+
def _in_tree_path(folder: Path, entry: str) -> Path:
|
|
555
|
+
path = folder / entry
|
|
556
|
+
# Python 3.15 on Windows resolves a path containing NUL instead of raising
|
|
557
|
+
try:
|
|
558
|
+
inside = "\0" not in entry and not Path(entry).is_absolute() and path.resolve().is_relative_to(folder.resolve())
|
|
559
|
+
except (OSError, RuntimeError): # RuntimeError: symlink loop before Python 3.13
|
|
560
|
+
inside = False
|
|
561
|
+
if not inside:
|
|
562
|
+
msg = f"build-system.backend-path entry {entry!r} must be a relative path inside the project {folder}"
|
|
563
|
+
raise ValueError(msg)
|
|
564
|
+
return path
|
|
565
|
+
|
|
566
|
+
|
|
542
567
|
__all__ = [
|
|
543
568
|
"BackendFailed",
|
|
544
569
|
"CmdStatus",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.12.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 12, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -2,10 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import subprocess # ruff:ignore[suspicious-subprocess-import]
|
|
6
|
+
import sys
|
|
7
|
+
from textwrap import dedent
|
|
5
8
|
from typing import TYPE_CHECKING, Any
|
|
6
9
|
|
|
7
10
|
import pytest
|
|
8
11
|
|
|
12
|
+
from pyproject_api import _backend
|
|
9
13
|
from pyproject_api._backend import BackendProxy, read_line, run
|
|
10
14
|
|
|
11
15
|
if TYPE_CHECKING:
|
|
@@ -130,6 +134,62 @@ def test_reuse_process(mocker: pytest_mock.MockerFixture, capsys: pytest.Capture
|
|
|
130
134
|
assert "SystemExit: 2" in captured.err
|
|
131
135
|
|
|
132
136
|
|
|
137
|
+
@pytest.mark.parametrize("code", ["Path('failure.txt')", "b'failure'"])
|
|
138
|
+
@pytest.mark.parametrize("previous_success", [False, True])
|
|
139
|
+
def test_reused_process_non_serializable_exit(tmp_path: Path, code: str, previous_success: bool) -> None:
|
|
140
|
+
(tmp_path / "exit_backend.py").write_text(
|
|
141
|
+
dedent(f"""
|
|
142
|
+
import os
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
|
|
145
|
+
calls = 0
|
|
146
|
+
|
|
147
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
148
|
+
global calls
|
|
149
|
+
calls += 1
|
|
150
|
+
print("hook pid={{}} calls={{}}".format(os.getpid(), calls))
|
|
151
|
+
if config_settings["fail"]:
|
|
152
|
+
raise SystemExit({code})
|
|
153
|
+
return "demo-{{}}.whl".format(calls)
|
|
154
|
+
""")
|
|
155
|
+
)
|
|
156
|
+
failures = [False, True, False] if previous_success else [True, False]
|
|
157
|
+
requests = [
|
|
158
|
+
{"cmd": "build_wheel", "kwargs": {"wheel_directory": str(tmp_path), "config_settings": {"fail": fail}}}
|
|
159
|
+
for fail in failures
|
|
160
|
+
]
|
|
161
|
+
requests.append({"cmd": "_exit", "kwargs": {}})
|
|
162
|
+
result_files = [tmp_path / f"response-{index}.json" for index in range(len(requests))]
|
|
163
|
+
messages = [
|
|
164
|
+
json.dumps({**request, "result": str(path)}) for request, path in zip(requests, result_files, strict=True)
|
|
165
|
+
]
|
|
166
|
+
completed = subprocess.run(
|
|
167
|
+
[sys.executable, _backend.__file__, "True", "exit_backend"],
|
|
168
|
+
input="\n".join(messages) + "\n",
|
|
169
|
+
capture_output=True,
|
|
170
|
+
text=True,
|
|
171
|
+
cwd=tmp_path,
|
|
172
|
+
env={**os.environ, "PYTHONPATH": str(tmp_path)},
|
|
173
|
+
timeout=30,
|
|
174
|
+
check=False,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Inspect the failed request first: the previous candidate returns the earlier wheel here.
|
|
178
|
+
failed = json.loads(result_files[failures.index(True)].read_text())
|
|
179
|
+
assert "return" not in failed
|
|
180
|
+
assert failed == {"code": 1, "exc_type": "RuntimeError", "exc_msg": "Failed to serialize backend exception"}
|
|
181
|
+
assert completed.returncode == 0, completed.stderr
|
|
182
|
+
for index, fail in enumerate(failures):
|
|
183
|
+
if not fail:
|
|
184
|
+
assert json.loads(result_files[index].read_text()) == {"return": f"demo-{index + 1}.whl"}
|
|
185
|
+
assert json.loads(result_files[-1].read_text()) == {"return": 0}
|
|
186
|
+
hook_lines = [line for line in completed.stdout.splitlines() if line.startswith("hook pid=")]
|
|
187
|
+
assert len(hook_lines) == len(failures)
|
|
188
|
+
assert len({line.split()[1] for line in hook_lines}) == 1
|
|
189
|
+
assert [line.split()[2] for line in hook_lines] == [f"calls={index + 1}" for index in range(len(failures))]
|
|
190
|
+
assert "SystemExit" in completed.stderr
|
|
191
|
+
|
|
192
|
+
|
|
133
193
|
def test_read_line_success() -> None:
|
|
134
194
|
r, w = os.pipe()
|
|
135
195
|
try:
|
|
@@ -5,7 +5,7 @@ import subprocess # ruff:ignore[suspicious-subprocess-import]
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from textwrap import dedent
|
|
8
|
-
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
from typing import TYPE_CHECKING, Final, Literal
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
from packaging.requirements import Requirement
|
|
@@ -16,6 +16,8 @@ from pyproject_api._via_fresh_subprocess import SubprocessFrontend
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from collections.abc import Callable
|
|
18
18
|
|
|
19
|
+
import pytest_mock
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
@pytest.fixture
|
|
21
23
|
def local_builder(tmp_path: Path) -> Callable[[str], Path]:
|
|
@@ -183,6 +185,38 @@ def test_empty_pyproject(tmp_path: Path) -> None:
|
|
|
183
185
|
assert str(left) == str(right)
|
|
184
186
|
|
|
185
187
|
|
|
188
|
+
@pytest.mark.parametrize(
|
|
189
|
+
"backend_path",
|
|
190
|
+
[
|
|
191
|
+
pytest.param("../outside", id="parent"),
|
|
192
|
+
pytest.param("{tmp}/outside", id="absolute-outside"),
|
|
193
|
+
pytest.param("{tmp}/project", id="absolute-inside"),
|
|
194
|
+
pytest.param("link", id="symlink-outside"),
|
|
195
|
+
pytest.param("\\u0000", id="nul"),
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
def test_create_args_from_folder_rejects_out_of_tree_backend_path(tmp_path: Path, backend_path: str) -> None:
|
|
199
|
+
project = tmp_path / "project"
|
|
200
|
+
project.mkdir()
|
|
201
|
+
(project / "link").symlink_to(tmp_path, target_is_directory=True)
|
|
202
|
+
(project / "pyproject.toml").write_text(
|
|
203
|
+
f'[build-system]\nbackend-path = ["{backend_path.format(tmp=tmp_path.as_posix())}"]\nbuild-backend = "b"\n'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
with pytest.raises(ValueError, match=r"backend-path entry .* must be a relative path inside the project"):
|
|
207
|
+
SubprocessFrontend.create_args_from_folder(project)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_create_args_from_folder_rejects_unresolvable_backend_path(
|
|
211
|
+
tmp_path: Path, mocker: pytest_mock.MockerFixture
|
|
212
|
+
) -> None:
|
|
213
|
+
(tmp_path / "pyproject.toml").write_text('[build-system]\nbackend-path = ["."]\nbuild-backend = "b"\n')
|
|
214
|
+
mocker.patch.object(Path, "resolve", autospec=True, side_effect=RuntimeError("Symlink loop"))
|
|
215
|
+
|
|
216
|
+
with pytest.raises(ValueError, match=r"backend-path entry '\.' must be a relative path inside the project"):
|
|
217
|
+
SubprocessFrontend.create_args_from_folder(tmp_path)
|
|
218
|
+
|
|
219
|
+
|
|
186
220
|
@pytest.fixture(scope="session")
|
|
187
221
|
def demo_pkg_inline() -> Path:
|
|
188
222
|
return Path(__file__).absolute().parent / "demo_pkg_inline"
|
|
@@ -232,19 +266,31 @@ def test_create_args_from_folder_rejects_bad_build_system_type(tmp_path: Path, t
|
|
|
232
266
|
SubprocessFrontend.create_args_from_folder(tmp_path)
|
|
233
267
|
|
|
234
268
|
|
|
235
|
-
|
|
236
|
-
|
|
269
|
+
@pytest.mark.parametrize(
|
|
270
|
+
"backend_obj",
|
|
271
|
+
[
|
|
272
|
+
pytest.param("backend", id="attribute"),
|
|
273
|
+
pytest.param("backend:", id="trailing-colon"),
|
|
274
|
+
pytest.param("holder.backend", id="nested"),
|
|
275
|
+
pytest.param("holder.nested.backend", id="deeply-nested"),
|
|
276
|
+
],
|
|
277
|
+
)
|
|
278
|
+
def test_backend_obj(tmp_path: Path, backend_obj: str) -> None:
|
|
279
|
+
toml = f"""
|
|
237
280
|
[build-system]
|
|
238
281
|
requires=[]
|
|
239
|
-
build-backend = "build.api:
|
|
282
|
+
build-backend = "build.api:{backend_obj}"
|
|
240
283
|
backend-path=["."]
|
|
241
284
|
"""
|
|
242
285
|
api = """
|
|
286
|
+
from types import SimpleNamespace
|
|
287
|
+
|
|
243
288
|
class A:
|
|
244
289
|
def get_requires_for_build_sdist(self, config_settings=None):
|
|
245
290
|
return ["a"]
|
|
246
291
|
|
|
247
292
|
backend = A()
|
|
293
|
+
holder = SimpleNamespace(backend=backend, nested=SimpleNamespace(backend=backend))
|
|
248
294
|
"""
|
|
249
295
|
(tmp_path / "pyproject.toml").write_text(dedent(toml))
|
|
250
296
|
build = tmp_path / "build"
|
|
@@ -253,9 +299,7 @@ def test_backend_obj(tmp_path: Path) -> None:
|
|
|
253
299
|
(build / "api.py").write_text(dedent(api))
|
|
254
300
|
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1])
|
|
255
301
|
result = frontend.get_requires_for_build_sdist()
|
|
256
|
-
|
|
257
|
-
assert isinstance(left, Requirement)
|
|
258
|
-
assert str(left) == str(right)
|
|
302
|
+
assert result.requires == (Requirement("a"),)
|
|
259
303
|
|
|
260
304
|
|
|
261
305
|
@pytest.mark.parametrize("of_type", ["wheel", "sdist"])
|
|
@@ -305,6 +349,35 @@ def test_bad_return_type_build_wheel(local_builder: Callable[[str], Path]) -> No
|
|
|
305
349
|
assert exc.exc_type == "TypeError"
|
|
306
350
|
|
|
307
351
|
|
|
352
|
+
@pytest.mark.parametrize("value", ["Path('demo.whl')", "b'demo.whl'", "{'demo.whl'}"])
|
|
353
|
+
def test_non_serializable_hook_result(value: str, local_builder: Callable[[str], Path]) -> None:
|
|
354
|
+
root = local_builder(f"""
|
|
355
|
+
import sys
|
|
356
|
+
from pathlib import Path
|
|
357
|
+
|
|
358
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
359
|
+
print("hook stdout")
|
|
360
|
+
print("hook stderr", file=sys.stderr)
|
|
361
|
+
if config_settings is None:
|
|
362
|
+
return {value}
|
|
363
|
+
return "demo-1.0-py3-none-any.whl"
|
|
364
|
+
""")
|
|
365
|
+
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(root)[:-1])
|
|
366
|
+
|
|
367
|
+
with pytest.raises(BackendFailed) as context:
|
|
368
|
+
frontend.build_wheel(root / "dist")
|
|
369
|
+
|
|
370
|
+
exc = context.value
|
|
371
|
+
assert exc.code == 1
|
|
372
|
+
assert exc.exc_type == "TypeError"
|
|
373
|
+
assert "not JSON serializable" in exc.exc_msg
|
|
374
|
+
assert "hook stdout" in exc.out
|
|
375
|
+
assert "hook stderr" in exc.err
|
|
376
|
+
assert "TypeError" in exc.err
|
|
377
|
+
result = frontend.build_wheel(root / "dist", config_settings={})
|
|
378
|
+
assert result.wheel.name == "demo-1.0-py3-none-any.whl"
|
|
379
|
+
|
|
380
|
+
|
|
308
381
|
def test_bad_return_type_prepare_metadata_for_build_wheel(local_builder: Callable[[str], Path]) -> None:
|
|
309
382
|
tmp_path = local_builder("def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): return 1")
|
|
310
383
|
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1])
|
|
@@ -327,6 +400,46 @@ def test_prepare_metadata_for_build_wheel_meta_is_root(local_builder: Callable[[
|
|
|
327
400
|
assert str(context.value) == f"the project root and the metadata directory can't be the same {tmp_path}"
|
|
328
401
|
|
|
329
402
|
|
|
403
|
+
@pytest.mark.parametrize("hook", ["prepare_metadata_for_build_wheel", "prepare_metadata_for_build_editable"])
|
|
404
|
+
@pytest.mark.parametrize("alias", [False, True], ids=["directory", "symlink"])
|
|
405
|
+
def test_prepare_metadata_rejects_ancestor_directory(tmp_path: Path, hook: str, alias: bool) -> None:
|
|
406
|
+
# The old cleanup can remove the whole ancestor, so keep it inside tmp_path.
|
|
407
|
+
ancestor: Final[Path] = tmp_path / "sandbox"
|
|
408
|
+
root: Final[Path] = ancestor / "project"
|
|
409
|
+
root.mkdir(parents=True)
|
|
410
|
+
source: Final[Path] = root / "build_tester.py"
|
|
411
|
+
source.write_text("source content\n")
|
|
412
|
+
unrelated: Final[Path] = ancestor / "unrelated.txt"
|
|
413
|
+
unrelated.write_text("keep me")
|
|
414
|
+
metadata_directory: Final[Path] = tmp_path / "alias" if alias else ancestor
|
|
415
|
+
if alias:
|
|
416
|
+
metadata_directory.symlink_to(ancestor, target_is_directory=True)
|
|
417
|
+
frontend: Final[SubprocessFrontend] = SubprocessFrontend(root, (root,), "build_tester", None, ())
|
|
418
|
+
|
|
419
|
+
with pytest.raises(RuntimeError, match="the metadata directory can't contain the project root"):
|
|
420
|
+
getattr(frontend, hook)(metadata_directory)
|
|
421
|
+
|
|
422
|
+
assert source.read_text() == "source content\n"
|
|
423
|
+
assert unrelated.read_text() == "keep me"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@pytest.mark.parametrize("hook", ["prepare_metadata_for_build_wheel", "prepare_metadata_for_build_editable"])
|
|
427
|
+
def test_prepare_metadata_allows_sibling_directory(tmp_path: Path, hook: str) -> None:
|
|
428
|
+
root: Final[Path] = tmp_path / "project"
|
|
429
|
+
root.mkdir()
|
|
430
|
+
source: Final[Path] = root / "build_tester.py"
|
|
431
|
+
source.write_text("")
|
|
432
|
+
metadata_directory: Final[Path] = tmp_path / "project-metadata"
|
|
433
|
+
metadata_directory.mkdir()
|
|
434
|
+
(metadata_directory / "stale.txt").write_text("remove me")
|
|
435
|
+
frontend: Final[SubprocessFrontend] = SubprocessFrontend(root, (root,), "build_tester", None, ())
|
|
436
|
+
|
|
437
|
+
assert getattr(frontend, hook)(metadata_directory) is None
|
|
438
|
+
|
|
439
|
+
assert source.is_file()
|
|
440
|
+
assert list(metadata_directory.iterdir()) == []
|
|
441
|
+
|
|
442
|
+
|
|
330
443
|
def test_no_wheel_prepare_metadata_for_build_wheel(local_builder: Callable[[str], Path]) -> None:
|
|
331
444
|
txt = "def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return 'out'"
|
|
332
445
|
tmp_path = local_builder(txt)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|