Skip to content

Commit 72d611a

Browse files
pybind11_abseil authorscopybara-github
authored andcommitted
New function CallAndCatchPybind11Exceptions, to convert all pybind11 exceptions into absl::Status.
PiperOrigin-RevId: 858213368
1 parent 54b34dd commit 72d611a

5 files changed

Lines changed: 164 additions & 0 deletions

File tree

pybind11_abseil/compat/BUILD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,13 @@ pybind_library(
4444
"@com_google_absl//absl/status",
4545
],
4646
)
47+
48+
pybind_library(
49+
name = "status_from_cpp_exc",
50+
hdrs = ["status_from_cpp_exc.h"],
51+
visibility = ["//visibility:public"],
52+
deps = [
53+
":status_from_py_exc",
54+
"@com_google_absl//absl/log:check",
55+
],
56+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#ifndef PYBIND11_ABSEIL_COMPAT_STATUS_FROM_CPP_EXC_H_
2+
#define PYBIND11_ABSEIL_COMPAT_STATUS_FROM_CPP_EXC_H_
3+
4+
#include <cassert>
5+
6+
#include "third_party/pybind11/include/pybind11/pytypes.h"
7+
#include "pybind11_abseil/compat/status_from_py_exc.h"
8+
9+
namespace pybind11_abseil::compat {
10+
11+
// This function is intended for C++ exception-aware code like the one using
12+
// pybind11 API, which needs to be called from an exception-free library like
13+
// the majority of Google's C++ codebase.
14+
//
15+
// It is not needed for pybind11 extension modules: After a call to
16+
// `ImportStatusModule()`, code inside a `m.def()` call automatically
17+
// installs exception handlers and performs the same conversion.
18+
//
19+
// This wrapper executes the given function, catches potential C++ exceptions
20+
// and converts them to absl::Status.
21+
//
22+
// The status code depends on the exception type (see
23+
// GetPyExceptionStatusCodeMap).
24+
// The status message always starts with the Python exception class name.
25+
// The Python traceback is not preserved.
26+
template <typename Func>
27+
inline decltype(std::declval<Func>()()) CallAndCatchPybind11Exceptions(
28+
Func&& func) {
29+
assert(PyGILState_Check());
30+
try {
31+
return std::forward<Func>(func)();
32+
} catch (pybind11::error_already_set& e) {
33+
e.restore();
34+
return StatusFromPyExcGivenErrOccurred();
35+
} catch (pybind11::builtin_exception& e) {
36+
e.set_error();
37+
return StatusFromPyExcGivenErrOccurred();
38+
}
39+
}
40+
41+
} // namespace pybind11_abseil::compat
42+
43+
#endif // PYBIND11_ABSEIL_COMPAT_STATUS_FROM_CPP_EXC_H_

pybind11_abseil/tests/BUILD

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,23 @@ py_test(
133133
],
134134
deps = [requirement("absl_py")],
135135
)
136+
137+
pybind_extension(
138+
name = "status_from_cpp_exc_test_lib",
139+
testonly = 1,
140+
srcs = ["status_from_cpp_exc_test_lib.cc"],
141+
deps = [
142+
"//pybind11_abseil/compat:status_from_cpp_exc",
143+
"@com_google_absl//absl/status",
144+
"@com_google_absl//absl/strings",
145+
],
146+
)
147+
148+
py_test(
149+
name = "status_from_cpp_exc_test",
150+
srcs = ["status_from_cpp_exc_test.py"],
151+
deps = [
152+
":status_from_cpp_exc_test_lib",
153+
"//testing/pybase",
154+
],
155+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Tests for the CallAndCatchPybind11Exceptions wrapper function."""
2+
3+
from google3.testing.pybase import googletest
4+
from pybind11_abseil.tests import status_from_cpp_exc_test_lib
5+
6+
7+
class StatusFromCppExcTest(googletest.TestCase):
8+
9+
def test_catches_error_already_set(self):
10+
statuses = status_from_cpp_exc_test_lib.CollectVariousStatuses()
11+
12+
def clean_source_location_trace(s):
13+
return s.partition(r'=== Source Location Trace: ===')[0]
14+
statuses = [clean_source_location_trace(s) for s in statuses]
15+
16+
self.assertEqual(statuses[0], 'OUT_OF_RANGE: ValueError: test error\n')
17+
self.assertEqual(statuses[1], 'OUT_OF_RANGE: ValueError: test error 2\n')
18+
self.assertStartsWith(
19+
statuses[2],
20+
'UNKNOWN: RuntimeError: Unable to cast Python instance of type'
21+
" <class 'int'> to ",
22+
)
23+
self.assertEqual(
24+
statuses[3], 'RESOURCE_EXHAUSTED: test error 3\n'
25+
)
26+
27+
28+
if __name__ == '__main__':
29+
googletest.main()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#include <string>
2+
#include <vector>
3+
4+
#include "absl/status/status.h"
5+
#include "absl/strings/str_cat.h"
6+
#include "third_party/pybind11/include/pybind11/pybind11.h"
7+
#include "third_party/pybind11/include/pybind11/stl.h"
8+
#include "pybind11_abseil/compat/status_from_cpp_exc.h"
9+
10+
namespace pybind11_abseil::compat {
11+
12+
namespace {
13+
14+
absl::Status CatchesErrorAlreadySet() {
15+
pybind11::gil_scoped_acquire gil;
16+
return CallAndCatchPybind11Exceptions([&]() -> absl::Status {
17+
PyErr_SetString(PyExc_ValueError, "test error");
18+
throw pybind11::error_already_set();
19+
});
20+
}
21+
22+
absl::Status CatchesBuiltinException() {
23+
pybind11::gil_scoped_acquire gil;
24+
return CallAndCatchPybind11Exceptions(
25+
[&]() -> absl::Status { throw pybind11::value_error("test error 2"); });
26+
}
27+
28+
absl::Status CatchesCastError() {
29+
pybind11::gil_scoped_acquire gil;
30+
return CallAndCatchPybind11Exceptions([&]() -> absl::Status {
31+
pybind11::object o = pybind11::int_(1);
32+
o.cast<std::string>();
33+
return absl::OkStatus();
34+
});
35+
}
36+
37+
absl::Status CatchesStatusNotOk() {
38+
pybind11::gil_scoped_acquire gil;
39+
return CallAndCatchPybind11Exceptions([&]() -> absl::Status {
40+
return absl::ResourceExhaustedError("test error 3");
41+
});
42+
}
43+
44+
// This function shows that Python and pybind11 exceptions can be caught, and
45+
// transferred to code that follows Google conventions (i.e. never use C++
46+
// exceptions, but use absl::Status instead).
47+
std::vector<std::string> CollectVariousStatuses() {
48+
return {
49+
absl::StrCat(CatchesErrorAlreadySet()),
50+
absl::StrCat(CatchesBuiltinException()),
51+
absl::StrCat(CatchesCastError()),
52+
absl::StrCat(CatchesStatusNotOk()),
53+
};
54+
}
55+
56+
} // namespace
57+
58+
PYBIND11_MODULE(status_from_cpp_exc_test_lib, m) {
59+
m.def("CollectVariousStatuses", &CollectVariousStatuses);
60+
}
61+
62+
} // namespace pybind11_abseil::compat

0 commit comments

Comments
 (0)