diff --git a/pybind11_abseil/compat/BUILD b/pybind11_abseil/compat/BUILD index 0c96ef1..e5dc3ec 100644 --- a/pybind11_abseil/compat/BUILD +++ b/pybind11_abseil/compat/BUILD @@ -44,3 +44,13 @@ pybind_library( "@com_google_absl//absl/status", ], ) + +pybind_library( + name = "status_from_cpp_exc", + hdrs = ["status_from_cpp_exc.h"], + visibility = ["//visibility:public"], + deps = [ + ":status_from_py_exc", + "@com_google_absl//absl/log:check", + ], +) diff --git a/pybind11_abseil/compat/status_from_cpp_exc.h b/pybind11_abseil/compat/status_from_cpp_exc.h new file mode 100644 index 0000000..c1489de --- /dev/null +++ b/pybind11_abseil/compat/status_from_cpp_exc.h @@ -0,0 +1,43 @@ +#ifndef PYBIND11_ABSEIL_COMPAT_STATUS_FROM_CPP_EXC_H_ +#define PYBIND11_ABSEIL_COMPAT_STATUS_FROM_CPP_EXC_H_ + +#include + +#include "third_party/pybind11/include/pybind11/pytypes.h" +#include "pybind11_abseil/compat/status_from_py_exc.h" + +namespace pybind11_abseil::compat { + +// This function is intended for C++ exception-aware code like the one using +// pybind11 API, which needs to be called from an exception-free library like +// the majority of Google's C++ codebase. +// +// It is not needed for pybind11 extension modules: After a call to +// `ImportStatusModule()`, code inside a `m.def()` call automatically +// installs exception handlers and performs the same conversion. +// +// This wrapper executes the given function, catches potential C++ exceptions +// and converts them to absl::Status. +// +// The status code depends on the exception type (see +// GetPyExceptionStatusCodeMap). +// The status message always starts with the Python exception class name. +// The Python traceback is not preserved. +template +inline decltype(std::declval()()) CallAndCatchPybind11Exceptions( + Func&& func) { + assert(PyGILState_Check()); + try { + return std::forward(func)(); + } catch (pybind11::error_already_set& e) { + e.restore(); + return StatusFromPyExcGivenErrOccurred(); + } catch (pybind11::builtin_exception& e) { + e.set_error(); + return StatusFromPyExcGivenErrOccurred(); + } +} + +} // namespace pybind11_abseil::compat + +#endif // PYBIND11_ABSEIL_COMPAT_STATUS_FROM_CPP_EXC_H_ diff --git a/pybind11_abseil/tests/BUILD b/pybind11_abseil/tests/BUILD index 48ae1f3..6b9e5c6 100644 --- a/pybind11_abseil/tests/BUILD +++ b/pybind11_abseil/tests/BUILD @@ -133,3 +133,23 @@ py_test( ], deps = [requirement("absl_py")], ) + +pybind_extension( + name = "status_from_cpp_exc_test_lib", + testonly = 1, + srcs = ["status_from_cpp_exc_test_lib.cc"], + deps = [ + "//pybind11_abseil/compat:status_from_cpp_exc", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + ], +) + +py_test( + name = "status_from_cpp_exc_test", + srcs = ["status_from_cpp_exc_test.py"], + deps = [ + ":status_from_cpp_exc_test_lib", + "//testing/pybase", + ], +) diff --git a/pybind11_abseil/tests/status_from_cpp_exc_test.py b/pybind11_abseil/tests/status_from_cpp_exc_test.py new file mode 100644 index 0000000..4316b47 --- /dev/null +++ b/pybind11_abseil/tests/status_from_cpp_exc_test.py @@ -0,0 +1,29 @@ +"""Tests for the CallAndCatchPybind11Exceptions wrapper function.""" + +from google3.testing.pybase import googletest +from pybind11_abseil.tests import status_from_cpp_exc_test_lib + + +class StatusFromCppExcTest(googletest.TestCase): + + def test_catches_error_already_set(self): + statuses = status_from_cpp_exc_test_lib.CollectVariousStatuses() + + def clean_source_location_trace(s): + return s.partition(r'=== Source Location Trace: ===')[0] + statuses = [clean_source_location_trace(s) for s in statuses] + + self.assertEqual(statuses[0], 'OUT_OF_RANGE: ValueError: test error\n') + self.assertEqual(statuses[1], 'OUT_OF_RANGE: ValueError: test error 2\n') + self.assertStartsWith( + statuses[2], + 'UNKNOWN: RuntimeError: Unable to cast Python instance of type' + " to ", + ) + self.assertEqual( + statuses[3], 'RESOURCE_EXHAUSTED: test error 3\n' + ) + + +if __name__ == '__main__': + googletest.main() diff --git a/pybind11_abseil/tests/status_from_cpp_exc_test_lib.cc b/pybind11_abseil/tests/status_from_cpp_exc_test_lib.cc new file mode 100644 index 0000000..2db85f0 --- /dev/null +++ b/pybind11_abseil/tests/status_from_cpp_exc_test_lib.cc @@ -0,0 +1,62 @@ +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "third_party/pybind11/include/pybind11/pybind11.h" +#include "third_party/pybind11/include/pybind11/stl.h" +#include "pybind11_abseil/compat/status_from_cpp_exc.h" + +namespace pybind11_abseil::compat { + +namespace { + +absl::Status CatchesErrorAlreadySet() { + pybind11::gil_scoped_acquire gil; + return CallAndCatchPybind11Exceptions([&]() -> absl::Status { + PyErr_SetString(PyExc_ValueError, "test error"); + throw pybind11::error_already_set(); + }); +} + +absl::Status CatchesBuiltinException() { + pybind11::gil_scoped_acquire gil; + return CallAndCatchPybind11Exceptions( + [&]() -> absl::Status { throw pybind11::value_error("test error 2"); }); +} + +absl::Status CatchesCastError() { + pybind11::gil_scoped_acquire gil; + return CallAndCatchPybind11Exceptions([&]() -> absl::Status { + pybind11::object o = pybind11::int_(1); + o.cast(); + return absl::OkStatus(); + }); +} + +absl::Status CatchesStatusNotOk() { + pybind11::gil_scoped_acquire gil; + return CallAndCatchPybind11Exceptions([&]() -> absl::Status { + return absl::ResourceExhaustedError("test error 3"); + }); +} + +// This function shows that Python and pybind11 exceptions can be caught, and +// transferred to code that follows Google conventions (i.e. never use C++ +// exceptions, but use absl::Status instead). +std::vector CollectVariousStatuses() { + return { + absl::StrCat(CatchesErrorAlreadySet()), + absl::StrCat(CatchesBuiltinException()), + absl::StrCat(CatchesCastError()), + absl::StrCat(CatchesStatusNotOk()), + }; +} + +} // namespace + +PYBIND11_MODULE(status_from_cpp_exc_test_lib, m) { + m.def("CollectVariousStatuses", &CollectVariousStatuses); +} + +} // namespace pybind11_abseil::compat