Skip to content

Commit c8564dd

Browse files
authored
PEP-660 support (#21)
1 parent 97e6196 commit c8564dd

File tree

8 files changed

+251
-12
lines changed

8 files changed

+251
-12
lines changed

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Release History
22
===============
33

4+
v1.1.0 - (2022-09-10)
5+
---------------------
6+
- PEP-660 support
7+
48
v1.0.0 - (2022-09-10)
59
---------------------
610
- Use hatchling as build backend

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ html.show_contexts = true
6565
html.skip_covered = false
6666
paths.source = [
6767
"src",
68-
".pyproject_api*/*/lib/python*/site-packages",
69-
".pyproject_api*/pypy*/site-packages",
70-
".pyproject_api*\\*\\Lib\\site-packages",
68+
".tox*/*/lib/python*/site-packages",
69+
".tox*/pypy*/site-packages",
70+
".tox*\\*\\Lib\\site-packages",
7171
"*/src",
7272
"*\\src",
7373
]

src/pyproject_api/_frontend.py

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ class RequiresBuildWheelResult(NamedTuple):
5959
err: str
6060

6161

62+
class RequiresBuildEditableResult(NamedTuple):
63+
"""Information collected while acquiring the wheel build dependencies"""
64+
65+
#: editable wheel build dependencies
66+
requires: tuple[Requirement, ...]
67+
#: backend standard output while acquiring the editable wheel build dependencies
68+
out: str
69+
#: backend standard error while acquiring the editable wheel build dependencies
70+
err: str
71+
72+
6273
class MetadataForBuildWheelResult(NamedTuple):
6374
"""Information collected while acquiring the wheel metadata"""
6475

@@ -70,6 +81,17 @@ class MetadataForBuildWheelResult(NamedTuple):
7081
err: str
7182

7283

84+
class MetadataForBuildEditableResult(NamedTuple):
85+
"""Information collected while acquiring the editable metadata"""
86+
87+
#: path to the wheel metadata
88+
metadata: Path
89+
#: backend standard output while generating the editable wheel metadata
90+
out: str
91+
#: backend standard output while generating the editable wheel metadata
92+
err: str
93+
94+
7395
class SdistResult(NamedTuple):
7496
"""Information collected while building a source distribution"""
7597

@@ -92,6 +114,17 @@ class WheelResult(NamedTuple):
92114
err: str
93115

94116

117+
class EditableResult(NamedTuple):
118+
"""Information collected while building an editable wheel"""
119+
120+
#: path to the built wheel artifact
121+
wheel: Path
122+
#: backend standard output while building the wheel
123+
out: str
124+
#: backend standard error while building the wheel
125+
err: str
126+
127+
95128
class BackendFailed(RuntimeError):
96129
"""An error of the build backend."""
97130

@@ -240,6 +273,23 @@ def get_requires_for_build_wheel(self, config_settings: ConfigSettings | None =
240273
self._unexpected_response("get_requires_for_build_wheel", result, "list of string", out, err)
241274
return RequiresBuildWheelResult(tuple(Requirement(r) for r in cast(List[str], result)), out, err)
242275

276+
def get_requires_for_build_editable(
277+
self, config_settings: ConfigSettings | None = None
278+
) -> RequiresBuildEditableResult:
279+
"""
280+
Get build requirements for an editable wheel build (per PEP-660).
281+
282+
:param config_settings: run arguments
283+
:return: outcome
284+
"""
285+
try:
286+
result, out, err = self._send(cmd="get_requires_for_build_editable", config_settings=config_settings)
287+
except BackendFailed as exc:
288+
result, out, err = [], exc.out, exc.err
289+
if not isinstance(result, list) or not all(isinstance(i, str) for i in result):
290+
self._unexpected_response("get_requires_for_build_editable", result, "list of string", out, err)
291+
return RequiresBuildEditableResult(tuple(Requirement(r) for r in cast(List[str], result)), out, err)
292+
243293
def prepare_metadata_for_build_wheel(
244294
self, metadata_directory: Path, config_settings: ConfigSettings | None = None
245295
) -> MetadataForBuildWheelResult:
@@ -250,24 +300,52 @@ def prepare_metadata_for_build_wheel(
250300
:param config_settings: build arguments
251301
:return: metadata generation result
252302
"""
303+
self._check_metadata_dir(metadata_directory)
304+
try:
305+
basename, out, err = self._send(
306+
cmd="prepare_metadata_for_build_wheel",
307+
metadata_directory=metadata_directory,
308+
config_settings=config_settings,
309+
)
310+
except BackendFailed:
311+
# if backend does not provide it acquire it from the wheel
312+
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory, "build_wheel")
313+
if not isinstance(basename, str):
314+
self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err)
315+
result = metadata_directory / basename
316+
return MetadataForBuildWheelResult(result, out, err)
317+
318+
def _check_metadata_dir(self, metadata_directory: Path) -> None:
253319
if metadata_directory == self._root:
254320
raise RuntimeError(f"the project root and the metadata directory can't be the same {self._root}")
255321
if metadata_directory.exists(): # start with fresh
256322
ensure_empty_dir(metadata_directory)
257323
metadata_directory.mkdir(parents=True, exist_ok=True)
324+
325+
def prepare_metadata_for_build_editable(
326+
self, metadata_directory: Path, config_settings: ConfigSettings | None = None
327+
) -> MetadataForBuildEditableResult:
328+
"""
329+
Build editable wheel metadata (per PEP-660).
330+
331+
:param metadata_directory: where to generate the metadata
332+
:param config_settings: build arguments
333+
:return: metadata generation result
334+
"""
335+
self._check_metadata_dir(metadata_directory)
258336
try:
259337
basename, out, err = self._send(
260-
cmd="prepare_metadata_for_build_wheel",
338+
cmd="prepare_metadata_for_build_editable",
261339
metadata_directory=metadata_directory,
262340
config_settings=config_settings,
263341
)
264342
except BackendFailed:
265343
# if backend does not provide it acquire it from the wheel
266-
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory)
344+
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory, "build_editable")
267345
if not isinstance(basename, str):
268346
self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err)
269347
result = metadata_directory / basename
270-
return MetadataForBuildWheelResult(result, out, err)
348+
return MetadataForBuildEditableResult(result, out, err)
271349

272350
def build_sdist(self, sdist_directory: Path, config_settings: ConfigSettings | None = None) -> SdistResult:
273351
"""
@@ -294,7 +372,7 @@ def build_wheel(
294372
metadata_directory: Path | None = None,
295373
) -> WheelResult:
296374
"""
297-
Build a source distribution (per PEP-517).
375+
Build a wheel file (per PEP-517).
298376
299377
:param wheel_directory: the folder where to build the wheel
300378
:param config_settings: build arguments
@@ -312,15 +390,40 @@ def build_wheel(
312390
self._unexpected_response("build_wheel", basename, str, out, err)
313391
return WheelResult(wheel_directory / basename, out, err)
314392

393+
def build_editable(
394+
self,
395+
wheel_directory: Path,
396+
config_settings: ConfigSettings | None = None,
397+
metadata_directory: Path | None = None,
398+
) -> EditableResult:
399+
"""
400+
Build an editable wheel file (per PEP-660).
401+
402+
:param wheel_directory: the folder where to build the editable wheel
403+
:param config_settings: build arguments
404+
:param metadata_directory: wheel metadata folder
405+
:return: wheel build result
406+
"""
407+
wheel_directory.mkdir(parents=True, exist_ok=True)
408+
basename, out, err = self._send(
409+
cmd="build_editable",
410+
wheel_directory=wheel_directory,
411+
config_settings=config_settings,
412+
metadata_directory=metadata_directory,
413+
)
414+
if not isinstance(basename, str):
415+
self._unexpected_response("build_editable", basename, str, out, err)
416+
return EditableResult(wheel_directory / basename, out, err)
417+
315418
def _unexpected_response(self, cmd: str, got: Any, expected_type: Any, out: str, err: str) -> NoReturn:
316419
msg = f"{cmd!r} on {self.backend!r} returned {got!r} but expected type {expected_type!r}"
317420
raise BackendFailed({"code": None, "exc_type": TypeError.__name__, "exc_msg": msg}, out, err)
318421

319422
def _metadata_from_built_wheel(
320-
self, config_settings: ConfigSettings | None, metadata_directory: Path | None
423+
self, config_settings: ConfigSettings | None, metadata_directory: Path | None, cmd: str
321424
) -> tuple[str, str, str]:
322425
with self._wheel_directory() as wheel_directory:
323-
wheel_result = self.build_wheel(
426+
wheel_result = getattr(self, cmd)(
324427
wheel_directory=wheel_directory,
325428
config_settings=config_settings,
326429
metadata_directory=metadata_directory,

tests/demo_pkg_inline/build.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,30 @@ def build_sdist(sdist_directory, config_settings=None): # noqa: U100
8181

8282
def get_requires_for_build_sdist(config_settings=None): # noqa: U100
8383
return [] # pragma: no cover # only executed in non-host pythons
84+
85+
86+
if "HAS_REQUIRES_EDITABLE" in os.environ:
87+
88+
def get_requires_for_build_editable(config_settings=...): # noqa: U100
89+
return [1] if "REQUIRES_EDITABLE_BAD_RETURN" in os.environ else ["editables"]
90+
91+
92+
if "HAS_PREPARE_EDITABLE" in os.environ:
93+
94+
def prepare_metadata_for_build_editable(metadata_directory: str, config_settings=None) -> str: # noqa: U100
95+
dest = os.path.join(metadata_directory, dist_info)
96+
os.mkdir(dest)
97+
for arc_name, data in content.items():
98+
if arc_name.startswith(dist_info):
99+
with open(os.path.join(metadata_directory, arc_name), "wt") as file_handler:
100+
file_handler.write(dedent(data).strip())
101+
print("created metadata {}".format(dest))
102+
if "PREPARE_EDITABLE_BAD" in os.environ:
103+
return 1 # type: ignore # checking bad type on purpose
104+
return dist_info
105+
106+
107+
def build_editable(wheel_directory, metadata_directory=None, config_settings=None) -> str:
108+
if "BUILD_EDITABLE_BAD" in os.environ:
109+
return 1 # type: ignore # checking bad type on purpose
110+
return build_wheel(wheel_directory, metadata_directory, config_settings)

tests/demo_pkg_inline/build.pyi

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ dist_info: str = ...
55
content: dict[str, str] = ...
66

77
def build_wheel(
8-
wheel_directory: str,
9-
metadata_directory: str | None = ...,
10-
config_settings: dict[str, str] | None = ...,
8+
wheel_directory: str, metadata_directory: str | None = ..., config_settings: dict[str, str] | None = ...
119
) -> str: ...
1210
def get_requires_for_build_wheel(config_settings: dict[str, str] | None = ...) -> list[str]: ...
1311
def build_sdist(sdist_directory: str, config_settings: dict[str, str] | None = ...) -> str: ...
1412
def get_requires_for_build_sdist(config_settings: dict[str, str] | None = ...) -> list[str]: ...
13+
def get_requires_for_build_editable(config_settings: dict[str, str] | None = ...) -> list[str]: ...
14+
def prepare_metadata_for_build_editable(
15+
metadata_directory: str, config_settings: dict[str, str] | None = None
16+
) -> str: ...
17+
def build_editable(
18+
wheel_directory: str, metadata_directory: str | None = ..., config_settings: dict[str, str] | None = ...
19+
) -> str: ...

tests/demo_pkg_inline/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
requires = []
33
build-backend = "build"
44
backend-path = ["."]
5+
6+
[tool.black]
7+
line-length = 120

tests/test_fronted.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,98 @@ def test_create_no_pyproject(tmp_path: Path) -> None:
211211
assert all(isinstance(i, Requirement) for i in result[4])
212212
assert [str(i) for i in result[4]] == ["setuptools>=40.8.0", "wheel"]
213213
assert result[5] is True
214+
215+
216+
def test_backend_get_requires_for_build_editable(demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
217+
monkeypatch.setenv("HAS_REQUIRES_EDITABLE", "1")
218+
monkeypatch.delenv("REQUIRES_EDITABLE_BAD_RETURN", raising=False)
219+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
220+
result = fronted.get_requires_for_build_editable()
221+
assert [str(i) for i in result.requires] == ["editables"]
222+
assert isinstance(result.requires[0], Requirement)
223+
assert " get_requires_for_build_editable " in result.out
224+
assert not result.err
225+
226+
227+
def test_backend_get_requires_for_build_editable_miss(demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
228+
monkeypatch.delenv("HAS_REQUIRES_EDITABLE", raising=False)
229+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
230+
result = fronted.get_requires_for_build_editable()
231+
assert not result.requires
232+
assert " get_requires_for_build_editable " in result.out
233+
assert not result.err
234+
235+
236+
def test_backend_get_requires_for_build_editable_bad(demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
237+
monkeypatch.setenv("HAS_REQUIRES_EDITABLE", "1")
238+
monkeypatch.setenv("REQUIRES_EDITABLE_BAD_RETURN", "1")
239+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
240+
with pytest.raises(BackendFailed) as context:
241+
fronted.get_requires_for_build_editable()
242+
exc = context.value
243+
assert exc.code is None
244+
assert not exc.err
245+
assert " get_requires_for_build_editable " in exc.out
246+
assert not exc.args
247+
assert exc.exc_type == "TypeError"
248+
assert exc.exc_msg == "'get_requires_for_build_editable' on 'build' returned [1] but expected type 'list of string'"
249+
250+
251+
def test_backend_prepare_editable(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
252+
monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1")
253+
monkeypatch.delenv("PREPARE_EDITABLE_BAD", raising=False)
254+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
255+
result = fronted.prepare_metadata_for_build_editable(tmp_path)
256+
assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info"
257+
assert " prepare_metadata_for_build_editable " in result.out
258+
assert " build_editable " not in result.out
259+
assert not result.err
260+
261+
262+
def test_backend_prepare_editable_miss(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
263+
monkeypatch.delenv("HAS_PREPARE_EDITABLE", raising=False)
264+
monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False)
265+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
266+
result = fronted.prepare_metadata_for_build_editable(tmp_path)
267+
assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info"
268+
assert " prepare_metadata_for_build_editable " not in result.out
269+
assert " build_editable " in result.out
270+
assert not result.err
271+
272+
273+
def test_backend_prepare_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
274+
monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1")
275+
monkeypatch.setenv("PREPARE_EDITABLE_BAD", "1")
276+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
277+
with pytest.raises(BackendFailed) as context:
278+
fronted.prepare_metadata_for_build_editable(tmp_path)
279+
exc = context.value
280+
assert exc.code is None
281+
assert not exc.err
282+
assert " prepare_metadata_for_build_editable " in exc.out
283+
assert not exc.args
284+
assert exc.exc_type == "TypeError"
285+
assert exc.exc_msg == "'prepare_metadata_for_build_wheel' on 'build' returned 1 but expected type <class 'str'>"
286+
287+
288+
def test_backend_build_editable(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
289+
monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False)
290+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
291+
result = fronted.build_editable(tmp_path)
292+
assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl"
293+
assert " build_editable " in result.out
294+
assert not result.err
295+
296+
297+
def test_backend_build_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
298+
monkeypatch.setenv("BUILD_EDITABLE_BAD", "1")
299+
fronted = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
300+
with pytest.raises(BackendFailed) as context:
301+
fronted.build_editable(tmp_path)
302+
exc = context.value
303+
assert exc.code is None
304+
assert not exc.err
305+
assert " build_editable " in exc.out
306+
assert not exc.args
307+
assert exc.exc_type == "TypeError"
308+
assert exc.exc_msg == "'build_editable' on 'build' returned 1 but expected type <class 'str'>"

whitelist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
autoclass
22
autodoc
33
cfg
4+
delenv
45
extlinks
56
intersphinx
67
iterdir
@@ -16,6 +17,7 @@ py38
1617
pygments
1718
pyproject
1819
sdist
20+
setenv
1921
tmpdir
2022
toml
2123
tomli

0 commit comments

Comments
 (0)