Skip to content

Commit 03b53bf

Browse files
vnlitvinovpre-commit-ci[bot]
authored andcommitted
Properly translate C++ exception to Python exception when creating Python buffer from wrapped object (#5324)
* Add test for throwing def_buffer case * Translate C++ -> Python exceptions in buffer creation This required a little refactoring to extract exception translation to a separate method * Fix code formatting * Fix "unused parameter" warning * Refactor per review * style: pre-commit fixes * Address review comments * Address review comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c09e245 commit 03b53bf

File tree

7 files changed

+108
-54
lines changed

7 files changed

+108
-54
lines changed

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ set(PYBIND11_HEADERS
155155
include/pybind11/detail/type_caster_base.h
156156
include/pybind11/detail/typeid.h
157157
include/pybind11/detail/value_and_holder.h
158+
include/pybind11/detail/exception_translation.h
158159
include/pybind11/attr.h
159160
include/pybind11/buffer_info.h
160161
include/pybind11/cast.h

include/pybind11/detail/class.h

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include <pybind11/attr.h>
1313
#include <pybind11/options.h>
1414

15+
#include "exception_translation.h"
16+
1517
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1618
PYBIND11_NAMESPACE_BEGIN(detail)
1719

@@ -591,7 +593,18 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla
591593
return -1;
592594
}
593595
std::memset(view, 0, sizeof(Py_buffer));
594-
buffer_info *info = tinfo->get_buffer(obj, tinfo->get_buffer_data);
596+
buffer_info *info = nullptr;
597+
try {
598+
info = tinfo->get_buffer(obj, tinfo->get_buffer_data);
599+
} catch (...) {
600+
try_translate_exceptions();
601+
raise_from(PyExc_BufferError, "Error getting buffer");
602+
return -1;
603+
}
604+
if (info == nullptr) {
605+
pybind11_fail("FATAL UNEXPECTED SITUATION: tinfo->get_buffer() returned nullptr.");
606+
}
607+
595608
if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE && info->readonly) {
596609
delete info;
597610
// view->obj = nullptr; // Was just memset to 0, so not necessary
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
pybind11/detail/exception_translation.h: means to translate C++ exceptions to Python exceptions
3+
4+
Copyright (c) 2024 The Pybind Development Team.
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#pragma once
11+
12+
#include "common.h"
13+
#include "internals.h"
14+
15+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
16+
PYBIND11_NAMESPACE_BEGIN(detail)
17+
18+
// Apply all the extensions translators from a list
19+
// Return true if one of the translators completed without raising an exception
20+
// itself. Return of false indicates that if there are other translators
21+
// available, they should be tried.
22+
inline bool apply_exception_translators(std::forward_list<ExceptionTranslator> &translators) {
23+
auto last_exception = std::current_exception();
24+
25+
for (auto &translator : translators) {
26+
try {
27+
translator(last_exception);
28+
return true;
29+
} catch (...) {
30+
last_exception = std::current_exception();
31+
}
32+
}
33+
return false;
34+
}
35+
36+
inline void try_translate_exceptions() {
37+
/* When an exception is caught, give each registered exception
38+
translator a chance to translate it to a Python exception. First
39+
all module-local translators will be tried in reverse order of
40+
registration. If none of the module-locale translators handle
41+
the exception (or there are no module-locale translators) then
42+
the global translators will be tried, also in reverse order of
43+
registration.
44+
45+
A translator may choose to do one of the following:
46+
47+
- catch the exception and call py::set_error()
48+
to set a standard (or custom) Python exception, or
49+
- do nothing and let the exception fall through to the next translator, or
50+
- delegate translation to the next translator by throwing a new type of exception.
51+
*/
52+
53+
bool handled = with_internals([&](internals &internals) {
54+
auto &local_exception_translators = get_local_internals().registered_exception_translators;
55+
if (detail::apply_exception_translators(local_exception_translators)) {
56+
return true;
57+
}
58+
auto &exception_translators = internals.registered_exception_translators;
59+
if (detail::apply_exception_translators(exception_translators)) {
60+
return true;
61+
}
62+
return false;
63+
});
64+
65+
if (!handled) {
66+
set_error(PyExc_SystemError, "Exception escaped from default exception translator!");
67+
}
68+
}
69+
70+
PYBIND11_NAMESPACE_END(detail)
71+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/pybind11.h

+2-53
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
*/
1010

1111
#pragma once
12-
1312
#include "detail/class.h"
13+
#include "detail/exception_translation.h"
1414
#include "detail/init.h"
1515
#include "attr.h"
1616
#include "gil.h"
@@ -95,24 +95,6 @@ inline std::string replace_newlines_and_squash(const char *text) {
9595
return result.substr(str_begin, str_range);
9696
}
9797

98-
// Apply all the extensions translators from a list
99-
// Return true if one of the translators completed without raising an exception
100-
// itself. Return of false indicates that if there are other translators
101-
// available, they should be tried.
102-
inline bool apply_exception_translators(std::forward_list<ExceptionTranslator> &translators) {
103-
auto last_exception = std::current_exception();
104-
105-
for (auto &translator : translators) {
106-
try {
107-
translator(last_exception);
108-
return true;
109-
} catch (...) {
110-
last_exception = std::current_exception();
111-
}
112-
}
113-
return false;
114-
}
115-
11698
#if defined(_MSC_VER)
11799
# define PYBIND11_COMPAT_STRDUP _strdup
118100
#else
@@ -1038,40 +1020,7 @@ class cpp_function : public function {
10381020
throw;
10391021
#endif
10401022
} catch (...) {
1041-
/* When an exception is caught, give each registered exception
1042-
translator a chance to translate it to a Python exception. First
1043-
all module-local translators will be tried in reverse order of
1044-
registration. If none of the module-locale translators handle
1045-
the exception (or there are no module-locale translators) then
1046-
the global translators will be tried, also in reverse order of
1047-
registration.
1048-
1049-
A translator may choose to do one of the following:
1050-
1051-
- catch the exception and call py::set_error()
1052-
to set a standard (or custom) Python exception, or
1053-
- do nothing and let the exception fall through to the next translator, or
1054-
- delegate translation to the next translator by throwing a new type of exception.
1055-
*/
1056-
1057-
bool handled = with_internals([&](internals &internals) {
1058-
auto &local_exception_translators
1059-
= get_local_internals().registered_exception_translators;
1060-
if (detail::apply_exception_translators(local_exception_translators)) {
1061-
return true;
1062-
}
1063-
auto &exception_translators = internals.registered_exception_translators;
1064-
if (detail::apply_exception_translators(exception_translators)) {
1065-
return true;
1066-
}
1067-
return false;
1068-
});
1069-
1070-
if (handled) {
1071-
return nullptr;
1072-
}
1073-
1074-
set_error(PyExc_SystemError, "Exception escaped from default exception translator!");
1023+
try_translate_exceptions();
10751024
return nullptr;
10761025
}
10771026

tests/extra_python_package/test_files.py

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"include/pybind11/detail/type_caster_base.h",
6060
"include/pybind11/detail/typeid.h",
6161
"include/pybind11/detail/value_and_holder.h",
62+
"include/pybind11/detail/exception_translation.h",
6263
}
6364

6465
eigen_headers = {

tests/test_buffers.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ TEST_SUBMODULE(buffers, m) {
167167
sizeof(float)});
168168
});
169169

170+
class BrokenMatrix : public Matrix {
171+
public:
172+
BrokenMatrix(py::ssize_t rows, py::ssize_t cols) : Matrix(rows, cols) {}
173+
void throw_runtime_error() { throw std::runtime_error("See PR #5324 for context."); }
174+
};
175+
py::class_<BrokenMatrix>(m, "BrokenMatrix", py::buffer_protocol())
176+
.def(py::init<py::ssize_t, py::ssize_t>())
177+
.def_buffer([](BrokenMatrix &m) {
178+
m.throw_runtime_error();
179+
return py::buffer_info();
180+
});
181+
170182
// test_inherited_protocol
171183
class SquareMatrix : public Matrix {
172184
public:

tests/test_buffers.py

+7
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,10 @@ def test_buffer_docstring():
228228
m.get_buffer_info.__doc__.strip()
229229
== "get_buffer_info(arg0: Buffer) -> pybind11_tests.buffers.buffer_info"
230230
)
231+
232+
233+
def test_buffer_exception():
234+
with pytest.raises(BufferError, match="Error getting buffer") as excinfo:
235+
memoryview(m.BrokenMatrix(1, 1))
236+
assert isinstance(excinfo.value.__cause__, RuntimeError)
237+
assert "for context" in str(excinfo.value.__cause__)

0 commit comments

Comments
 (0)