diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16166fb..09f32fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: diff --git a/docs/changelog.rst b/docs/changelog.rst index 5a62715..9664c74 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,29 @@ Release History =============== +v1.6.0 - (2023-08-29) +--------------------- +- Remove ``build_`` from ``prepare_metadata_for_build_`` 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`` diff --git a/pyproject.toml b/pyproject.toml index 7ce03df..c85b210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] @@ -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" @@ -104,7 +104,6 @@ paths.source = [ "*/src", "*\\src", ] -report.fail_under = 98 report.omit = [] run.parallel = true run.plugins = ["covdefaults"] diff --git a/src/pyproject_api/_backend.py b/src/pyproject_api/_backend.py index 0583d12..ac12c32 100644 --- a/src/pyproject_api/_backend.py +++ b/src/pyproject_api/_backend.py @@ -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": diff --git a/src/pyproject_api/_frontend.py b/src/pyproject_api/_frontend.py index 174fa92..efcf134 100644 --- a/src/pyproject_api/_frontend.py +++ b/src/pyproject_api/_frontend.py @@ -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 @@ -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). @@ -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: @@ -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). @@ -359,7 +357,7 @@ 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", @@ -367,8 +365,7 @@ def prepare_metadata_for_build_editable( 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 @@ -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]: diff --git a/tests/demo_pkg_inline/build.py b/tests/demo_pkg_inline/build.py index cb161a6..2134355 100644 --- a/tests/demo_pkg_inline/build.py +++ b/tests/demo_pkg_inline/build.py @@ -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): @@ -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 @@ -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 diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 433e0b5..4f6f6ec 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -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 @@ -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: @@ -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 @@ -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: @@ -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 @@ -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: @@ -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]) diff --git a/tests/test_frontend_setuptools.py b/tests/test_frontend_setuptools.py index 42009d4..92bfda2 100644 --- a/tests/test_frontend_setuptools.py +++ b/tests/test_frontend_setuptools.py @@ -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" @@ -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 diff --git a/tox.ini b/tox.ini index ad931f5..45fe629 100644 --- a/tox.ini +++ b/tox.ini @@ -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 =