From 13732346b1c2a37ede92f2c9d102749f46ab9730 Mon Sep 17 00:00:00 2001 From: Tejesh Vaish <188414140+tridev-tej@users.noreply.github.com> Date: Sun, 25 Oct 2026 06:23:07 +0400 Subject: [PATCH 1/3] Fix backend proxy stub declarations --- .../+backend-proxy-stubs.bugfix.rst | 1 + src/pyproject_api/_backend.pyi | 4 +-- tests/test_backend.py | 30 ++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 docs/changelog.d/+backend-proxy-stubs.bugfix.rst diff --git a/docs/changelog.d/+backend-proxy-stubs.bugfix.rst b/docs/changelog.d/+backend-proxy-stubs.bugfix.rst new file mode 100644 index 0000000..86d08cb --- /dev/null +++ b/docs/changelog.d/+backend-proxy-stubs.bugfix.rst @@ -0,0 +1 @@ +Correct the backend proxy type declarations for optional hooks and the exit return value. diff --git a/src/pyproject_api/_backend.pyi b/src/pyproject_api/_backend.pyi index 3142e06..4d02166 100644 --- a/src/pyproject_api/_backend.pyi +++ b/src/pyproject_api/_backend.pyi @@ -10,8 +10,8 @@ class BackendProxy: def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: ... - def _exit(self) -> None: ... - def _optional_commands(self) -> dict[str, bool]: ... + def _exit(self) -> int: ... + def _optional_hooks(self) -> dict[str, bool]: ... def run(argv: Sequence[str]) -> int: ... def read_line(fd: int = 0) -> bytearray: ... diff --git a/tests/test_backend.py b/tests/test_backend.py index a8f6437..4d81b79 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -5,7 +5,8 @@ import subprocess # ruff:ignore[suspicious-subprocess-import] import sys from textwrap import dedent -from typing import TYPE_CHECKING, Any +from types import ModuleType +from typing import TYPE_CHECKING, Any, assert_type import pytest @@ -18,6 +19,33 @@ import pytest_mock +def test_backend_proxy_optional_hooks_and_exit(monkeypatch: pytest.MonkeyPatch) -> None: + class CheckedBackendProxy(BackendProxy): + def check(self) -> None: + hooks = assert_type(self._optional_hooks(), dict[str, bool]) + assert hooks == { + "get_requires_for_build_sdist": True, + "prepare_metadata_for_build_wheel": False, + "get_requires_for_build_wheel": True, + "build_editable": False, + "get_requires_for_build_editable": True, + "prepare_metadata_for_build_editable": False, + } + exit_code = assert_type(self._exit(), int) + assert type(exit_code) is int + assert exit_code == 0 + + backend = ModuleType("stub_test_backend") + for hook in ("get_requires_for_build_sdist", "get_requires_for_build_wheel", "get_requires_for_build_editable"): + setattr(backend, hook, list) + monkeypatch.setitem(sys.modules, backend.__name__, backend) + proxy = CheckedBackendProxy(backend.__name__, None) + proxy.check() + assert not hasattr(proxy, "_optional_commands") + with pytest.raises(_backend.MissingCommand, match="_optional_commands"): + proxy("_optional_commands") + + def test_invalid_module(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(ImportError): run([str(False), "an.invalid.module"]) From fa4af9bf28f9e8781aee21be064674876a4449d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Tue, 27 Oct 2026 12:34:51 -0700 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=85=20test(backend):=20reduce=20the?= =?UTF-8?q?=20stub=20test=20to=20its=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The type checker is what catches a stub drifting from the module, so the test only needs to call both methods through assert_type. Use a stdlib module as the backend instead of a patched fake, and drop the checks for _optional_commands, which only the old stub declared. --- tests/test_backend.py | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 4d81b79..cda76ec 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -5,7 +5,6 @@ import subprocess # ruff:ignore[suspicious-subprocess-import] import sys from textwrap import dedent -from types import ModuleType from typing import TYPE_CHECKING, Any, assert_type import pytest @@ -19,31 +18,13 @@ import pytest_mock -def test_backend_proxy_optional_hooks_and_exit(monkeypatch: pytest.MonkeyPatch) -> None: - class CheckedBackendProxy(BackendProxy): +def test_backend_proxy_private_command_types() -> None: + class TypedBackendProxy(BackendProxy): def check(self) -> None: - hooks = assert_type(self._optional_hooks(), dict[str, bool]) - assert hooks == { - "get_requires_for_build_sdist": True, - "prepare_metadata_for_build_wheel": False, - "get_requires_for_build_wheel": True, - "build_editable": False, - "get_requires_for_build_editable": True, - "prepare_metadata_for_build_editable": False, - } - exit_code = assert_type(self._exit(), int) - assert type(exit_code) is int - assert exit_code == 0 - - backend = ModuleType("stub_test_backend") - for hook in ("get_requires_for_build_sdist", "get_requires_for_build_wheel", "get_requires_for_build_editable"): - setattr(backend, hook, list) - monkeypatch.setitem(sys.modules, backend.__name__, backend) - proxy = CheckedBackendProxy(backend.__name__, None) - proxy.check() - assert not hasattr(proxy, "_optional_commands") - with pytest.raises(_backend.MissingCommand, match="_optional_commands"): - proxy("_optional_commands") + assert assert_type(self._optional_hooks(), dict[str, bool])["build_editable"] is False + assert assert_type(self._exit(), int) == 0 + + TypedBackendProxy("json", None).check() def test_invalid_module(capsys: pytest.CaptureFixture[str]) -> None: From 53895bfd149045ce1ab58edd13259bc603446dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Tue, 27 Oct 2026 16:22:32 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix(backend):=20dro?= =?UTF-8?q?p=20the=20private=20proxy=20stub=20declarations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No typed code calls the proxy's private commands; the frontend sends them by name over the backend protocol. Removing the declarations fixes the wrong one without keeping a test that reaches into private methods. --- docs/changelog.d/+backend-proxy-stubs.bugfix.rst | 2 +- src/pyproject_api/_backend.pyi | 2 -- tests/test_backend.py | 11 +---------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/docs/changelog.d/+backend-proxy-stubs.bugfix.rst b/docs/changelog.d/+backend-proxy-stubs.bugfix.rst index 86d08cb..acc0a82 100644 --- a/docs/changelog.d/+backend-proxy-stubs.bugfix.rst +++ b/docs/changelog.d/+backend-proxy-stubs.bugfix.rst @@ -1 +1 @@ -Correct the backend proxy type declarations for optional hooks and the exit return value. +Drop the backend proxy stub declarations for its private commands; one named a method the proxy does not have. diff --git a/src/pyproject_api/_backend.pyi b/src/pyproject_api/_backend.pyi index 4d02166..4e35087 100644 --- a/src/pyproject_api/_backend.pyi +++ b/src/pyproject_api/_backend.pyi @@ -10,8 +10,6 @@ class BackendProxy: def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: ... - def _exit(self) -> int: ... - def _optional_hooks(self) -> dict[str, bool]: ... def run(argv: Sequence[str]) -> int: ... def read_line(fd: int = 0) -> bytearray: ... diff --git a/tests/test_backend.py b/tests/test_backend.py index cda76ec..a8f6437 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -5,7 +5,7 @@ import subprocess # ruff:ignore[suspicious-subprocess-import] import sys from textwrap import dedent -from typing import TYPE_CHECKING, Any, assert_type +from typing import TYPE_CHECKING, Any import pytest @@ -18,15 +18,6 @@ import pytest_mock -def test_backend_proxy_private_command_types() -> None: - class TypedBackendProxy(BackendProxy): - def check(self) -> None: - assert assert_type(self._optional_hooks(), dict[str, bool])["build_editable"] is False - assert assert_type(self._exit(), int) == 0 - - TypedBackendProxy("json", None).check() - - def test_invalid_module(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(ImportError): run([str(False), "an.invalid.module"])