Skip to content

Commit

Permalink
Extrat build_<wheel|editable> from prepare_metadata_for_build_<wheel|…
Browse files Browse the repository at this point in the history
…editable> (#99)
  • Loading branch information
gaborbernat authored Aug 29, 2023
1 parent 03c47f7 commit 0b2c1b7
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ repos:
rev: "1.1.0"
hooks:
- id: pyproject-fmt
additional_dependencies: ["tox>=4.6.4"]
additional_dependencies: ["tox>=4.10"]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.2"
hooks:
Expand Down
23 changes: 23 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
Release History
===============

v1.6.0 - (2023-08-29)
---------------------
- Remove ``build_<wheel|editable>`` from ``prepare_metadata_for_build_<wheel|editable>`` to allow separate config
parametrization and instead add :meth:`pyproject_api.Frontend.metadata_from_built` the user can call when the prepare
fails. Pass ``None`` for ``metadata_directory`` for such temporary wheel builds.

v1.5.4 - (2023-08-17)
---------------------
- Make sure that the order of Requires-Dist does not matter

v1.5.3 - (2023-07-06)
---------------------
- Fix ``read_line`` to raise ``EOFError`` if nothing was read

v1.5.2 - (2023-06-14)
---------------------
- Use ruff for linting.
- Drop 2.7 test run.

v1.5.1 - (2023-03-12)
---------------------
- docs: set html_last_updated_fmt to format string

v1.5.0 - (2023-01-17)
---------------------
- When getting metadata from a built wheel, do not pass ``metadata_directory``
Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ dependencies = [
'tomli>=2.0.1; python_version < "3.11"',
]
optional-dependencies.docs = [
"furo>=2023.7.26",
"furo>=2023.8.19",
"sphinx<7.2",
"sphinx-autodoc-typehints>=1.24",
]
Expand All @@ -55,8 +55,8 @@ optional-dependencies.testing = [
"pytest>=7.4",
"pytest-cov>=4.1",
"pytest-mock>=3.11.1",
"setuptools>=68",
"wheel>=0.41.1",
"setuptools>=68.1.2",
"wheel>=0.41.2",
]
urls.Homepage = "http://pyproject_api.readthedocs.org"
urls.Source = "https://github.com/tox-dev/pyproject-api"
Expand Down Expand Up @@ -104,7 +104,6 @@ paths.source = [
"*/src",
"*\\src",
]
report.fail_under = 98
report.omit = []
run.parallel = true
run.plugins = ["covdefaults"]
Expand Down
2 changes: 1 addition & 1 deletion src/pyproject_api/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def read_line(fd=0):
if not char:
if not content:
raise EOFError("EOF without reading anything") # we didn't get a line at all, let the caller know
break
break # pragma: no cover
if char == b"\n":
break
if char != b"\r":
Expand Down
61 changes: 32 additions & 29 deletions src/pyproject_api/_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
from time import sleep
from typing import Any, Dict, Iterator, List, NamedTuple, NoReturn, Optional, TypedDict, cast
from typing import Any, Dict, Iterator, List, Literal, NamedTuple, NoReturn, Optional, TypedDict, cast
from zipfile import ZipFile

from packaging.requirements import Requirement
Expand Down Expand Up @@ -314,7 +314,7 @@ def prepare_metadata_for_build_wheel(
self,
metadata_directory: Path,
config_settings: ConfigSettings | None = None,
) -> MetadataForBuildWheelResult:
) -> MetadataForBuildWheelResult | None:
"""
Build wheel metadata (per PEP-517).
Expand All @@ -331,12 +331,10 @@ def prepare_metadata_for_build_wheel(
config_settings=config_settings,
)
if basename is None:
# if backend does not provide it acquire it from the wheel
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory, "build_wheel")
return None
if not isinstance(basename, str):
self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err)
result = metadata_directory / basename
return MetadataForBuildWheelResult(result, out, err)
return MetadataForBuildWheelResult(metadata_directory / basename, out, err)

def _check_metadata_dir(self, metadata_directory: Path) -> None:
if metadata_directory == self._root:
Expand All @@ -350,7 +348,7 @@ def prepare_metadata_for_build_editable(
self,
metadata_directory: Path,
config_settings: ConfigSettings | None = None,
) -> MetadataForBuildEditableResult:
) -> MetadataForBuildEditableResult | None:
"""
Build editable wheel metadata (per PEP-660).
Expand All @@ -359,16 +357,15 @@ def prepare_metadata_for_build_editable(
:return: metadata generation result
"""
self._check_metadata_dir(metadata_directory)
basename = None
basename: str | None = None
if self.optional_hooks["prepare_metadata_for_build_editable"]:
basename, out, err = self._send(
cmd="prepare_metadata_for_build_editable",
metadata_directory=metadata_directory,
config_settings=config_settings,
)
if basename is None:
# if backend does not provide it acquire it from the wheel
basename, err, out = self._metadata_from_built_wheel(config_settings, metadata_directory, "build_editable")
return None
if not isinstance(basename, str):
self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err)
result = metadata_directory / basename
Expand Down Expand Up @@ -453,35 +450,41 @@ def _unexpected_response( # noqa: PLR0913
msg = f"{cmd!r} on {self.backend!r} returned {got!r} but expected type {expected_type!r}"
raise BackendFailed({"code": None, "exc_type": TypeError.__name__, "exc_msg": msg}, out, err)

def _metadata_from_built_wheel(
def metadata_from_built(
self,
config_settings: ConfigSettings | None,
metadata_directory: Path | None,
cmd: str,
) -> tuple[str, str, str]:
metadata_directory: Path,
target: Literal["wheel", "editable"],
config_settings: ConfigSettings | None = None,
) -> tuple[Path, str, str]:
"""
Create metadata from building the wheel (use when the prepare endpoints are not present or don't work).
:param metadata_directory: directory where to put the metadata
:param target: the type of wheel metadata to build
:param config_settings: config settings to pass in to the build endpoint
:return:
"""
hook = getattr(self, f"build_{target}")
with self._wheel_directory() as wheel_directory:
wheel_result = getattr(self, cmd)(
wheel_directory=wheel_directory,
config_settings=config_settings,
metadata_directory=None, # let the backend populate the metadata
)
wheel = wheel_result.wheel
result: EditableResult | WheelResult = hook(wheel_directory, config_settings)
wheel = result.wheel
if not wheel.exists():
msg = f"missing wheel file return by backed {wheel!r}"
raise RuntimeError(msg)
out, err = wheel_result.out, wheel_result.err
out, err = result.out, result.err
extract_to = str(metadata_directory)
basename = None
with ZipFile(str(wheel), "r") as zip_file:
for name in zip_file.namelist(): # pragma: no branch
path = Path(name)
if path.parts[0].endswith(".dist-info"):
basename = path.parts[0]
root = Path(name).parts[0]
if root.endswith(".dist-info"):
basename = root
zip_file.extract(name, extract_to)
if basename is None: # pragma: no branch
msg = f"no .dist-info found inside generated wheel {wheel}"
raise RuntimeError(msg)
return basename, err, out
break
if basename is None: # pragma: no branch
msg = f"no .dist-info found inside generated wheel {wheel}"
raise RuntimeError(msg)
return metadata_directory / basename, out, err

@contextmanager
def _wheel_directory(self) -> Iterator[Path]:
Expand Down
9 changes: 4 additions & 5 deletions tests/demo_pkg_inline/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def build_wheel(
base_name = f"{name}-{version}-py{sys.version_info[0]}-none-any.whl"
path = Path(wheel_directory) / base_name
with ZipFile(str(path), "w") as zip_file_handler:
for arc_name, data in content.items(): # pragma: no branch
for arc_name, data in content.items():
zip_file_handler.writestr(arc_name, dedent(data).strip())
if metadata_directory is not None:
for sub_directory, _, filenames in os.walk(metadata_directory):
Expand All @@ -72,7 +72,7 @@ def build_wheel(
str(Path(sub_directory) / filename),
)
else:
for arc_name, data in metadata.items(): # pragma: no branch
for arc_name, data in metadata.items():
zip_file_handler.writestr(arc_name, dedent(data).strip())
print(f"created wheel {path}") # noqa: T201
return base_name
Expand Down Expand Up @@ -109,9 +109,8 @@ def prepare_metadata_for_build_editable(
) -> str:
dest = Path(metadata_directory) / dist_info
dest.mkdir(parents=True)
for arc_name, data in content.items():
if arc_name.startswith(dist_info):
(dest.parent / arc_name).write_text(dedent(data).strip())
for arc_name, data in metadata.items():
(dest.parent / arc_name).write_text(dedent(data).strip())
print(f"created metadata {dest}") # noqa: T201
if "PREPARE_EDITABLE_BAD" in os.environ:
return 1 # type: ignore[return-value] # checking bad type on purpose
Expand Down
48 changes: 38 additions & 10 deletions tests/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pathlib import Path
from textwrap import dedent
from typing import Callable
from typing import Callable, Literal

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


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

with pytest.raises(RuntimeError, match="missing wheel file return by backed *"):
frontend.prepare_metadata_for_build_wheel(tmp_path / "meta")
frontend.metadata_from_built(tmp_path, "wheel")


@pytest.mark.parametrize("target", ["wheel", "editable"])
def test_metadata_from_built_wheel(
demo_pkg_inline: Path,
tmp_path: Path,
target: Literal["wheel", "editable"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
monkeypatch.chdir(tmp_path)
path, out, err = frontend.metadata_from_built(tmp_path, target)
assert path == tmp_path / "demo_pkg_inline-1.0.0.dist-info"
assert f" build_{target}" in out
assert not err


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

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


def test_create_no_pyproject(tmp_path: Path) -> None:
Expand Down Expand Up @@ -253,6 +268,7 @@ def test_backend_prepare_editable(tmp_path: Path, demo_pkg_inline: Path, monkeyp
monkeypatch.delenv("PREPARE_EDITABLE_BAD", raising=False)
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
result = frontend.prepare_metadata_for_build_editable(tmp_path)
assert result is not None
assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info"
assert " prepare_metadata_for_build_editable " in result.out
assert " build_editable " not in result.out
Expand All @@ -264,10 +280,7 @@ def test_backend_prepare_editable_miss(tmp_path: Path, demo_pkg_inline: Path, mo
monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False)
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
result = frontend.prepare_metadata_for_build_editable(tmp_path)
assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info"
assert " prepare_metadata_for_build_editable " not in result.out
assert " build_editable " in result.out
assert not result.err
assert result is None


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

def test_backend_build_editable(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False)
monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1")
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
result = frontend.build_editable(tmp_path)
meta = tmp_path / "meta"
res = frontend.prepare_metadata_for_build_editable(meta)
assert res is not None
metadata = res.metadata
assert metadata is not None
assert metadata.name == "demo_pkg_inline-1.0.0.dist-info"
result = frontend.build_editable(tmp_path, metadata_directory=meta)
assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl"
assert " build_editable " in result.out
assert not result.err


def test_backend_build_wheel(tmp_path: Path, demo_pkg_inline: Path) -> None:
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
result = frontend.build_wheel(tmp_path)
assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl"
assert " build_wheel " in result.out
assert not result.err


def test_backend_build_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BUILD_EDITABLE_BAD", "1")
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1])
Expand Down
2 changes: 2 additions & 0 deletions tests/test_frontend_setuptools.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def test_setuptools_get_requires_for_build_wheel(frontend_setuptools: Subprocess
def test_setuptools_prepare_metadata_for_build_wheel(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None:
meta = tmp_path / "meta"
result = frontend_setuptools.prepare_metadata_for_build_wheel(metadata_directory=meta)
assert result is not None
dist = Distribution.at(str(result.metadata))
assert list(dist.entry_points) == [EntryPoint(name="demo_exe", value="demo:a", group="console_scripts")]
assert dist.version == "1.0"
Expand All @@ -82,6 +83,7 @@ def test_setuptools_prepare_metadata_for_build_wheel(frontend_setuptools: Subpro
# call it again regenerates it because frontend always deletes earlier content
before = result.metadata.stat().st_mtime
result = frontend_setuptools.prepare_metadata_for_build_wheel(metadata_directory=meta)
assert result is not None
after = result.metadata.stat().st_mtime
assert after > before

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ commands =
[testenv:type]
description = run type check on code base
deps =
mypy==1.4.1
mypy==1.5.1
set_env =
{tty:MYPY_FORCE_COLOR = 1}
commands =
Expand Down

0 comments on commit 0b2c1b7

Please sign in to comment.