Skip to content

Commit 0b2c1b7

Browse files
authored
Extrat build_<wheel|editable> from prepare_metadata_for_build_<wheel|editable> (#99)
1 parent 03c47f7 commit 0b2c1b7

File tree

9 files changed

+105
-51
lines changed

9 files changed

+105
-51
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
rev: "1.1.0"
2424
hooks:
2525
- id: pyproject-fmt
26-
additional_dependencies: ["tox>=4.6.4"]
26+
additional_dependencies: ["tox>=4.10"]
2727
- repo: https://github.com/pre-commit/mirrors-prettier
2828
rev: "v3.0.2"
2929
hooks:

docs/changelog.rst

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

4+
v1.6.0 - (2023-08-29)
5+
---------------------
6+
- Remove ``build_<wheel|editable>`` from ``prepare_metadata_for_build_<wheel|editable>`` to allow separate config
7+
parametrization and instead add :meth:`pyproject_api.Frontend.metadata_from_built` the user can call when the prepare
8+
fails. Pass ``None`` for ``metadata_directory`` for such temporary wheel builds.
9+
10+
v1.5.4 - (2023-08-17)
11+
---------------------
12+
- Make sure that the order of Requires-Dist does not matter
13+
14+
v1.5.3 - (2023-07-06)
15+
---------------------
16+
- Fix ``read_line`` to raise ``EOFError`` if nothing was read
17+
18+
v1.5.2 - (2023-06-14)
19+
---------------------
20+
- Use ruff for linting.
21+
- Drop 2.7 test run.
22+
23+
v1.5.1 - (2023-03-12)
24+
---------------------
25+
- docs: set html_last_updated_fmt to format string
26+
427
v1.5.0 - (2023-01-17)
528
---------------------
629
- When getting metadata from a built wheel, do not pass ``metadata_directory``

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies = [
4646
'tomli>=2.0.1; python_version < "3.11"',
4747
]
4848
optional-dependencies.docs = [
49-
"furo>=2023.7.26",
49+
"furo>=2023.8.19",
5050
"sphinx<7.2",
5151
"sphinx-autodoc-typehints>=1.24",
5252
]
@@ -55,8 +55,8 @@ optional-dependencies.testing = [
5555
"pytest>=7.4",
5656
"pytest-cov>=4.1",
5757
"pytest-mock>=3.11.1",
58-
"setuptools>=68",
59-
"wheel>=0.41.1",
58+
"setuptools>=68.1.2",
59+
"wheel>=0.41.2",
6060
]
6161
urls.Homepage = "http://pyproject_api.readthedocs.org"
6262
urls.Source = "https://github.com/tox-dev/pyproject-api"
@@ -104,7 +104,6 @@ paths.source = [
104104
"*/src",
105105
"*\\src",
106106
]
107-
report.fail_under = 98
108107
report.omit = []
109108
run.parallel = true
110109
run.plugins = ["covdefaults"]

src/pyproject_api/_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def read_line(fd=0):
120120
if not char:
121121
if not content:
122122
raise EOFError("EOF without reading anything") # we didn't get a line at all, let the caller know
123-
break
123+
break # pragma: no cover
124124
if char == b"\n":
125125
break
126126
if char != b"\r":

src/pyproject_api/_frontend.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from tempfile import NamedTemporaryFile, TemporaryDirectory
1010
from time import sleep
11-
from typing import Any, Dict, Iterator, List, NamedTuple, NoReturn, Optional, TypedDict, cast
11+
from typing import Any, Dict, Iterator, List, Literal, NamedTuple, NoReturn, Optional, TypedDict, cast
1212
from zipfile import ZipFile
1313

1414
from packaging.requirements import Requirement
@@ -314,7 +314,7 @@ def prepare_metadata_for_build_wheel(
314314
self,
315315
metadata_directory: Path,
316316
config_settings: ConfigSettings | None = None,
317-
) -> MetadataForBuildWheelResult:
317+
) -> MetadataForBuildWheelResult | None:
318318
"""
319319
Build wheel metadata (per PEP-517).
320320
@@ -331,12 +331,10 @@ def prepare_metadata_for_build_wheel(
331331
config_settings=config_settings,
332332
)
333333
if basename is None:
334-
# if backend does not provide it acquire it from the wheel
335-
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory, "build_wheel")
334+
return None
336335
if not isinstance(basename, str):
337336
self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err)
338-
result = metadata_directory / basename
339-
return MetadataForBuildWheelResult(result, out, err)
337+
return MetadataForBuildWheelResult(metadata_directory / basename, out, err)
340338

341339
def _check_metadata_dir(self, metadata_directory: Path) -> None:
342340
if metadata_directory == self._root:
@@ -350,7 +348,7 @@ def prepare_metadata_for_build_editable(
350348
self,
351349
metadata_directory: Path,
352350
config_settings: ConfigSettings | None = None,
353-
) -> MetadataForBuildEditableResult:
351+
) -> MetadataForBuildEditableResult | None:
354352
"""
355353
Build editable wheel metadata (per PEP-660).
356354
@@ -359,16 +357,15 @@ def prepare_metadata_for_build_editable(
359357
:return: metadata generation result
360358
"""
361359
self._check_metadata_dir(metadata_directory)
362-
basename = None
360+
basename: str | None = None
363361
if self.optional_hooks["prepare_metadata_for_build_editable"]:
364362
basename, out, err = self._send(
365363
cmd="prepare_metadata_for_build_editable",
366364
metadata_directory=metadata_directory,
367365
config_settings=config_settings,
368366
)
369367
if basename is None:
370-
# if backend does not provide it acquire it from the wheel
371-
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory, "build_editable")
368+
return None
372369
if not isinstance(basename, str):
373370
self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err)
374371
result = metadata_directory / basename
@@ -453,35 +450,41 @@ def _unexpected_response( # noqa: PLR0913
453450
msg = f"{cmd!r} on {self.backend!r} returned {got!r} but expected type {expected_type!r}"
454451
raise BackendFailed({"code": None, "exc_type": TypeError.__name__, "exc_msg": msg}, out, err)
455452

456-
def _metadata_from_built_wheel(
453+
def metadata_from_built(
457454
self,
458-
config_settings: ConfigSettings | None,
459-
metadata_directory: Path | None,
460-
cmd: str,
461-
) -> tuple[str, str, str]:
455+
metadata_directory: Path,
456+
target: Literal["wheel", "editable"],
457+
config_settings: ConfigSettings | None = None,
458+
) -> tuple[Path, str, str]:
459+
"""
460+
Create metadata from building the wheel (use when the prepare endpoints are not present or don't work).
461+
462+
:param metadata_directory: directory where to put the metadata
463+
:param target: the type of wheel metadata to build
464+
:param config_settings: config settings to pass in to the build endpoint
465+
:return:
466+
"""
467+
hook = getattr(self, f"build_{target}")
462468
with self._wheel_directory() as wheel_directory:
463-
wheel_result = getattr(self, cmd)(
464-
wheel_directory=wheel_directory,
465-
config_settings=config_settings,
466-
metadata_directory=None, # let the backend populate the metadata
467-
)
468-
wheel = wheel_result.wheel
469+
result: EditableResult | WheelResult = hook(wheel_directory, config_settings)
470+
wheel = result.wheel
469471
if not wheel.exists():
470472
msg = f"missing wheel file return by backed {wheel!r}"
471473
raise RuntimeError(msg)
472-
out, err = wheel_result.out, wheel_result.err
474+
out, err = result.out, result.err
473475
extract_to = str(metadata_directory)
474476
basename = None
475477
with ZipFile(str(wheel), "r") as zip_file:
476478
for name in zip_file.namelist(): # pragma: no branch
477-
path = Path(name)
478-
if path.parts[0].endswith(".dist-info"):
479-
basename = path.parts[0]
479+
root = Path(name).parts[0]
480+
if root.endswith(".dist-info"):
481+
basename = root
480482
zip_file.extract(name, extract_to)
481-
if basename is None: # pragma: no branch
482-
msg = f"no .dist-info found inside generated wheel {wheel}"
483-
raise RuntimeError(msg)
484-
return basename, err, out
483+
break
484+
if basename is None: # pragma: no branch
485+
msg = f"no .dist-info found inside generated wheel {wheel}"
486+
raise RuntimeError(msg)
487+
return metadata_directory / basename, out, err
485488

486489
@contextmanager
487490
def _wheel_directory(self) -> Iterator[Path]:

tests/demo_pkg_inline/build.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def build_wheel(
6262
base_name = f"{name}-{version}-py{sys.version_info[0]}-none-any.whl"
6363
path = Path(wheel_directory) / base_name
6464
with ZipFile(str(path), "w") as zip_file_handler:
65-
for arc_name, data in content.items(): # pragma: no branch
65+
for arc_name, data in content.items():
6666
zip_file_handler.writestr(arc_name, dedent(data).strip())
6767
if metadata_directory is not None:
6868
for sub_directory, _, filenames in os.walk(metadata_directory):
@@ -72,7 +72,7 @@ def build_wheel(
7272
str(Path(sub_directory) / filename),
7373
)
7474
else:
75-
for arc_name, data in metadata.items(): # pragma: no branch
75+
for arc_name, data in metadata.items():
7676
zip_file_handler.writestr(arc_name, dedent(data).strip())
7777
print(f"created wheel {path}") # noqa: T201
7878
return base_name
@@ -109,9 +109,8 @@ def prepare_metadata_for_build_editable(
109109
) -> str:
110110
dest = Path(metadata_directory) / dist_info
111111
dest.mkdir(parents=True)
112-
for arc_name, data in content.items():
113-
if arc_name.startswith(dist_info):
114-
(dest.parent / arc_name).write_text(dedent(data).strip())
112+
for arc_name, data in metadata.items():
113+
(dest.parent / arc_name).write_text(dedent(data).strip())
115114
print(f"created metadata {dest}") # noqa: T201
116115
if "PREPARE_EDITABLE_BAD" in os.environ:
117116
return 1 # type: ignore[return-value] # checking bad type on purpose

tests/test_frontend.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pathlib import Path
44
from textwrap import dedent
5-
from typing import Callable
5+
from typing import Callable, Literal
66

77
import pytest
88
from packaging.requirements import Requirement
@@ -68,7 +68,7 @@ def demo_pkg_inline() -> Path:
6868
def test_backend_no_prepare_wheel(tmp_path: Path, demo_pkg_inline: Path) -> None:
6969
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
7070
result = frontend.prepare_metadata_for_build_wheel(tmp_path)
71-
assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info"
71+
assert result is None
7272

7373

7474
def test_backend_build_sdist_demo_pkg_inline(tmp_path: Path, demo_pkg_inline: Path) -> None:
@@ -178,10 +178,25 @@ def test_no_wheel_prepare_metadata_for_build_wheel(local_builder: Callable[[str]
178178
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1])
179179

180180
with pytest.raises(RuntimeError, match="missing wheel file return by backed *"):
181-
frontend.prepare_metadata_for_build_wheel(tmp_path / "meta")
181+
frontend.metadata_from_built(tmp_path, "wheel")
182+
183+
184+
@pytest.mark.parametrize("target", ["wheel", "editable"])
185+
def test_metadata_from_built_wheel(
186+
demo_pkg_inline: Path,
187+
tmp_path: Path,
188+
target: Literal["wheel", "editable"],
189+
monkeypatch: pytest.MonkeyPatch,
190+
) -> None:
191+
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
192+
monkeypatch.chdir(tmp_path)
193+
path, out, err = frontend.metadata_from_built(tmp_path, target)
194+
assert path == tmp_path / "demo_pkg_inline-1.0.0.dist-info"
195+
assert f" build_{target}" in out
196+
assert not err
182197

183198

184-
def test_bad_wheel_prepare_metadata_for_build_wheel(local_builder: Callable[[str], Path]) -> None:
199+
def test_bad_wheel_metadata_from_built_wheel(local_builder: Callable[[str], Path]) -> None:
185200
txt = """
186201
import sys
187202
from pathlib import Path
@@ -198,7 +213,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
198213
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1])
199214

200215
with pytest.raises(RuntimeError, match="no .dist-info found inside generated wheel*"):
201-
frontend.prepare_metadata_for_build_wheel(tmp_path / "meta")
216+
frontend.metadata_from_built(tmp_path, "wheel")
202217

203218

204219
def test_create_no_pyproject(tmp_path: Path) -> None:
@@ -253,6 +268,7 @@ def test_backend_prepare_editable(tmp_path: Path, demo_pkg_inline: Path, monkeyp
253268
monkeypatch.delenv("PREPARE_EDITABLE_BAD", raising=False)
254269
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
255270
result = frontend.prepare_metadata_for_build_editable(tmp_path)
271+
assert result is not None
256272
assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info"
257273
assert " prepare_metadata_for_build_editable " in result.out
258274
assert " build_editable " not in result.out
@@ -264,10 +280,7 @@ def test_backend_prepare_editable_miss(tmp_path: Path, demo_pkg_inline: Path, mo
264280
monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False)
265281
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
266282
result = frontend.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
283+
assert result is None
271284

272285

273286
def test_backend_prepare_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -287,13 +300,28 @@ def test_backend_prepare_editable_bad(tmp_path: Path, demo_pkg_inline: Path, mon
287300

288301
def test_backend_build_editable(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
289302
monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False)
303+
monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1")
290304
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
291-
result = frontend.build_editable(tmp_path)
305+
meta = tmp_path / "meta"
306+
res = frontend.prepare_metadata_for_build_editable(meta)
307+
assert res is not None
308+
metadata = res.metadata
309+
assert metadata is not None
310+
assert metadata.name == "demo_pkg_inline-1.0.0.dist-info"
311+
result = frontend.build_editable(tmp_path, metadata_directory=meta)
292312
assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl"
293313
assert " build_editable " in result.out
294314
assert not result.err
295315

296316

317+
def test_backend_build_wheel(tmp_path: Path, demo_pkg_inline: Path) -> None:
318+
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
319+
result = frontend.build_wheel(tmp_path)
320+
assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl"
321+
assert " build_wheel " in result.out
322+
assert not result.err
323+
324+
297325
def test_backend_build_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
298326
monkeypatch.setenv("BUILD_EDITABLE_BAD", "1")
299327
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])

tests/test_frontend_setuptools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def test_setuptools_get_requires_for_build_wheel(frontend_setuptools: Subprocess
6969
def test_setuptools_prepare_metadata_for_build_wheel(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None:
7070
meta = tmp_path / "meta"
7171
result = frontend_setuptools.prepare_metadata_for_build_wheel(metadata_directory=meta)
72+
assert result is not None
7273
dist = Distribution.at(str(result.metadata))
7374
assert list(dist.entry_points) == [EntryPoint(name="demo_exe", value="demo:a", group="console_scripts")]
7475
assert dist.version == "1.0"
@@ -82,6 +83,7 @@ def test_setuptools_prepare_metadata_for_build_wheel(frontend_setuptools: Subpro
8283
# call it again regenerates it because frontend always deletes earlier content
8384
before = result.metadata.stat().st_mtime
8485
result = frontend_setuptools.prepare_metadata_for_build_wheel(metadata_directory=meta)
86+
assert result is not None
8587
after = result.metadata.stat().st_mtime
8688
assert after > before
8789

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ commands =
4747
[testenv:type]
4848
description = run type check on code base
4949
deps =
50-
mypy==1.4.1
50+
mypy==1.5.1
5151
set_env =
5252
{tty:MYPY_FORCE_COLOR = 1}
5353
commands =

0 commit comments

Comments
 (0)