From e8c749c5536c46a42577d370cd695efa54e5db14 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 19 Jun 2023 20:32:45 +0100 Subject: [PATCH] Forward warnings from build-backend to build-frontend This makes it possible for build-frontends to surface this information to end users. --- src/pyproject_hooks/__init__.py | 2 + src/pyproject_hooks/_impl.py | 13 ++++++ .../_in_process/_in_process.py | 44 ++++++++++++------- .../buildsys_pkgs/buildsys_warnings.py | 9 ++++ .../samples/pkg-with-warnings/pyproject.toml | 3 ++ tests/test_call_hooks.py | 8 ++++ 6 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 tests/samples/buildsys_pkgs/buildsys_warnings.py create mode 100644 tests/samples/pkg-with-warnings/pyproject.toml diff --git a/src/pyproject_hooks/__init__.py b/src/pyproject_hooks/__init__.py index 9ae51a8..ba46b8a 100644 --- a/src/pyproject_hooks/__init__.py +++ b/src/pyproject_hooks/__init__.py @@ -2,6 +2,7 @@ """ from ._impl import ( + BuildBackendWarning, BackendInvalid, BackendUnavailable, BuildBackendHookCaller, @@ -13,6 +14,7 @@ __version__ = "1.0.0" __all__ = [ + "BuildBackendWarning", "BackendUnavailable", "BackendInvalid", "HookMissing", diff --git a/src/pyproject_hooks/_impl.py b/src/pyproject_hooks/_impl.py index 67e0ea7..56e68ec 100644 --- a/src/pyproject_hooks/_impl.py +++ b/src/pyproject_hooks/_impl.py @@ -6,6 +6,7 @@ from os.path import abspath from os.path import join as pjoin from subprocess import STDOUT, check_call, check_output +import warnings from ._in_process import _in_proc_script_path @@ -20,6 +21,10 @@ def read_json(path): return json.load(f) +class BuildBackendWarning(UserWarning): + """Will be emitted for every UserWarning emitted by the hook process.""" + + class BackendUnavailable(Exception): """Will be raised if the backend cannot be imported in the hook process.""" @@ -343,4 +348,12 @@ def _call_hook(self, hook_name, kwargs): ) if data.get("hook_missing"): raise HookMissing(data.get("missing_hook_name") or hook_name) + + for w in data.get("warnings", []): + warnings.warn_explicit( + message=w["message"], + category=BuildBackendWarning, + filename=w["filename"], + lineno=w["lineno"], + ) return data["return_val"] diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index fa0beae..1dde57a 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -22,6 +22,7 @@ from glob import glob from importlib import import_module from os.path import join as pjoin +import warnings # This file is run as a script, and `import wrappers` is not zip-safe, so we # include write_json() and read_json() from wrappers.py. @@ -336,22 +337,33 @@ def main(): hook_input = read_json(pjoin(control_dir, "input.json")) - json_out = {"unsupported": False, "return_val": None} - try: - json_out["return_val"] = hook(**hook_input["kwargs"]) - except BackendUnavailable as e: - json_out["no_backend"] = True - json_out["traceback"] = e.traceback - except BackendInvalid as e: - json_out["backend_invalid"] = True - json_out["backend_error"] = e.message - except GotUnsupportedOperation as e: - json_out["unsupported"] = True - json_out["traceback"] = e.traceback - except HookMissing as e: - json_out["hook_missing"] = True - json_out["missing_hook_name"] = e.hook_name or hook_name - + with warnings.catch_warnings(record=True) as captured_warnings: + json_out = {"unsupported": False, "return_val": None} + try: + json_out["return_val"] = hook(**hook_input["kwargs"]) + except BackendUnavailable as e: + json_out["no_backend"] = True + json_out["traceback"] = e.traceback + except BackendInvalid as e: + json_out["backend_invalid"] = True + json_out["backend_error"] = e.message + except GotUnsupportedOperation as e: + json_out["unsupported"] = True + json_out["traceback"] = e.traceback + except HookMissing as e: + json_out["hook_missing"] = True + json_out["missing_hook_name"] = e.hook_name or hook_name + + json_out["warnings"] = [ + { + "message": str(w.message), + "filename": w.filename, + "lineno": w.lineno, + } + for w in captured_warnings + if isinstance(w.category, type) and issubclass(w.category, UserWarning) + ] + print(json_out) write_json(json_out, pjoin(control_dir, "output.json"), indent=2) diff --git a/tests/samples/buildsys_pkgs/buildsys_warnings.py b/tests/samples/buildsys_pkgs/buildsys_warnings.py new file mode 100644 index 0000000..5d8336a --- /dev/null +++ b/tests/samples/buildsys_pkgs/buildsys_warnings.py @@ -0,0 +1,9 @@ +"""Test "backend" defining nothing other than a hook that logs a warning. +""" + +import warnings + + +def get_requires_for_build_wheel(config_settings): + warnings.warn("this is my example warning") + return [] diff --git a/tests/samples/pkg-with-warnings/pyproject.toml b/tests/samples/pkg-with-warnings/pyproject.toml new file mode 100644 index 0000000..3f5b51f --- /dev/null +++ b/tests/samples/pkg-with-warnings/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["eg_buildsys"] +build-backend = "buildsys_warnings" diff --git a/tests/test_call_hooks.py b/tests/test_call_hooks.py index b1ba351..5cbd677 100644 --- a/tests/test_call_hooks.py +++ b/tests/test_call_hooks.py @@ -12,6 +12,7 @@ from pyproject_hooks import ( BackendUnavailable, + BuildBackendWarning, BuildBackendHookCaller, UnsupportedOperation, default_subprocess_runner, @@ -226,3 +227,10 @@ def test__supported_features(pkg, expected): with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): res = hooks._supported_features() assert res == expected + + +def test_warnings(): + hooks = get_hooks("pkg-with-warnings") + with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): + with pytest.warns(BuildBackendWarning, match="this is my example warning"): + hooks.get_requires_for_build_wheel({})