Skip to content

Commit

Permalink
Use py::set_error() everywhere possible (only one special case, in …
Browse files Browse the repository at this point in the history
…common.h).

Overload `py::set_error(py::handle, py::handle)`.
Change back to `static py::handle exc = ... .release();`
Deprecate `py::exception<>::operator()`
  • Loading branch information
Ralf W. Grosse-Kunstleve committed Aug 4, 2023
1 parent e2746eb commit daaabcc
Show file tree
Hide file tree
Showing 15 changed files with 85 additions and 48 deletions.
18 changes: 8 additions & 10 deletions docs/advanced/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ before a global translator is tried.
Inside the translator, ``std::rethrow_exception`` should be used within
a try block to re-throw the exception. One or more catch clauses to catch
the appropriate exceptions should then be used with each clause using
``PyErr_SetString`` to set a Python exception or ``ex(string)`` to set
the python exception to a custom exception type (see below).
``py::set_error()`` (see below).

To declare a custom Python exception type, declare a ``py::exception`` variable
and use this in the associated exception translator (note: it is often useful
Expand All @@ -145,12 +144,12 @@ standard python RuntimeError:
// This is a static object, so we must leak the Python reference:
// It is undefined when the destructor will run, possibly only after the
// Python interpreter is finalized already.
static const auto *const exc = new py::exception<MyCustomException>(m, "MyCustomError");
static py::handle exc = py::exception<MyCustomException>(m, "MyCustomError").release();
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) std::rethrow_exception(p);
} catch (const MyCustomException &e) {
(*exc)(e.what());
py::set_error(exc, e.what());
} catch (const OtherException &e) {
py::set_error(PyExc_RuntimeError, e.what());
}
Expand All @@ -171,8 +170,7 @@ section.

.. note::

Call either ``PyErr_SetString`` or a custom exception's call
operator (``exc(string)``) for every exception caught in a custom exception
Call ``py::set_error()`` for every exception caught in a custom exception
translator. Failure to do so will cause Python to crash with ``SystemError:
error return without exception set``.

Expand Down Expand Up @@ -203,7 +201,7 @@ If module1 has the following translator:
try {
if (p) std::rethrow_exception(p);
} catch (const std::invalid_argument &e) {
PyErr_SetString("module1 handled this")
py::set_error(PyExc_ArgumentError, "module1 handled this");
}
}
Expand All @@ -215,7 +213,7 @@ and module2 has the following similar translator:
try {
if (p) std::rethrow_exception(p);
} catch (const std::invalid_argument &e) {
PyErr_SetString("module2 handled this")
py::set_error(PyExc_ArgumentError, "module2 handled this");
}
}
Expand Down Expand Up @@ -315,11 +313,11 @@ error protocol, which is outlined here.
After calling the Python C API, if Python returns an error,
``throw py::error_already_set();``, which allows pybind11 to deal with the
exception and pass it back to the Python interpreter. This includes calls to
the error setting functions such as ``PyErr_SetString``.
the error setting functions such as ``py::set_error()``.

.. code-block:: cpp
PyErr_SetString(PyExc_TypeError, "C API type error demo");
py::set_error(PyExc_TypeError, "C API type error demo");
throw py::error_already_set();
// But it would be easier to simply...
Expand Down
6 changes: 3 additions & 3 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *,
extern "C" inline int pybind11_object_init(PyObject *self, PyObject *, PyObject *) {
PyTypeObject *type = Py_TYPE(self);
std::string msg = get_fully_qualified_tp_name(type) + ": No constructor defined!";
PyErr_SetString(PyExc_TypeError, msg.c_str());
set_error(PyExc_TypeError, msg.c_str());
return -1;
}

Expand Down Expand Up @@ -579,15 +579,15 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla
if (view) {
view->obj = nullptr;
}
PyErr_SetString(PyExc_BufferError, "pybind11_getbuffer(): Internal error");
set_error(PyExc_BufferError, "pybind11_getbuffer(): Internal error");
return -1;
}
std::memset(view, 0, sizeof(Py_buffer));
buffer_info *info = tinfo->get_buffer(obj, tinfo->get_buffer_data);
if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE && info->readonly) {
delete info;
// view->obj = nullptr; // Was just memset to 0, so not necessary
PyErr_SetString(PyExc_BufferError, "Writable buffer requested for readonly storage");
set_error(PyExc_BufferError, "Writable buffer requested for readonly storage");
return -1;
}
view->obj = obj;
Expand Down
2 changes: 1 addition & 1 deletion include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ PYBIND11_WARNING_POP
return nullptr; \
} \
catch (const std::exception &e) { \
PyErr_SetString(PyExc_ImportError, e.what()); \
::pybind11::set_error(PyExc_ImportError, e.what()); \
return nullptr; \
}

Expand Down
2 changes: 1 addition & 1 deletion include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ inline bool raise_err(PyObject *exc_type, const char *msg) {
raise_from(exc_type, msg);
return true;
}
PyErr_SetString(exc_type, msg);
set_error(exc_type, msg);
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ class type_caster_generic {
std::string tname = rtti_type ? rtti_type->name() : cast_type.name();
detail::clean_type_id(tname);
std::string msg = "Unregistered type : " + tname;
PyErr_SetString(PyExc_TypeError, msg.c_str());
set_error(PyExc_TypeError, msg.c_str());
return {nullptr, nullptr};
}

Expand Down
4 changes: 2 additions & 2 deletions include/pybind11/numpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ class array : public buffer {
/// Create array from any object -- always returns a new reference
static PyObject *raw_array(PyObject *ptr, int ExtraFlags = 0) {
if (ptr == nullptr) {
PyErr_SetString(PyExc_ValueError, "cannot create a pybind11::array from a nullptr");
set_error(PyExc_ValueError, "cannot create a pybind11::array from a nullptr");
return nullptr;
}
return detail::npy_api::get().PyArray_FromAny_(
Expand Down Expand Up @@ -1155,7 +1155,7 @@ class array_t : public array {
/// Create array from any object -- always returns a new reference
static PyObject *raw_array_t(PyObject *ptr) {
if (ptr == nullptr) {
PyErr_SetString(PyExc_ValueError, "cannot create a pybind11::array_t from a nullptr");
set_error(PyExc_ValueError, "cannot create a pybind11::array_t from a nullptr");
return nullptr;
}
return detail::npy_api::get().PyArray_FromAny_(ptr,
Expand Down
24 changes: 10 additions & 14 deletions include/pybind11/pybind11.h
Original file line number Diff line number Diff line change
Expand Up @@ -694,9 +694,8 @@ class cpp_function : public function {
if (overloads->is_constructor) {
if (!parent
|| !PyObject_TypeCheck(parent.ptr(), (PyTypeObject *) overloads->scope.ptr())) {
PyErr_SetString(
PyExc_TypeError,
"__init__(self, ...) called with invalid or missing `self` argument");
set_error(PyExc_TypeError,
"__init__(self, ...) called with invalid or missing `self` argument");
return nullptr;
}

Expand Down Expand Up @@ -1007,7 +1006,7 @@ class cpp_function : public function {
A translator may choose to do one of the following:
- catch the exception and call PyErr_SetString or PyErr_SetObject
- catch the exception and call py::set_error()
to set a standard (or custom) Python exception, or
- do nothing and let the exception fall through to the next translator, or
- delegate translation to the next translator by throwing a new type of exception.
Expand All @@ -1023,8 +1022,7 @@ class cpp_function : public function {
return nullptr;
}

PyErr_SetString(PyExc_SystemError,
"Exception escaped from default exception translator!");
set_error(PyExc_SystemError, "Exception escaped from default exception translator!");
return nullptr;
}

Expand Down Expand Up @@ -1125,7 +1123,7 @@ class cpp_function : public function {
raise_from(PyExc_TypeError, msg.c_str());
return nullptr;
}
PyErr_SetString(PyExc_TypeError, msg.c_str());
set_error(PyExc_TypeError, msg.c_str());
return nullptr;
}
if (!result) {
Expand All @@ -1138,7 +1136,7 @@ class cpp_function : public function {
raise_from(PyExc_TypeError, msg.c_str());
return nullptr;
}
PyErr_SetString(PyExc_TypeError, msg.c_str());
set_error(PyExc_TypeError, msg.c_str());
return nullptr;
}
if (overloads->is_constructor && !self_value_and_holder.holder_constructed()) {
Expand Down Expand Up @@ -2525,14 +2523,10 @@ inline void register_local_exception_translator(ExceptionTranslator &&translator
std::forward<ExceptionTranslator>(translator));
}

inline void set_error(const handle exc, const char *message) {
PyErr_SetString(exc.ptr(), message);
}

/**
* Wrapper to generate a new Python exception type.
*
* This should only be used with PyErr_SetString for now.
* This should only be used with py::set_error() for now.
* It is not (yet) possible to use as a py::base.
* Template type argument is reserved for future use.
*/
Expand All @@ -2553,6 +2547,8 @@ class exception : public object {
}

// Sets the current python exception to this exception object with the given message
PYBIND11_DEPRECATED("Please use py::set_error() instead "
"(https://github.com/pybind/pybind11/pull/4772)")
void operator()(const char *message) const { set_error(*this, message); }
};

Expand Down Expand Up @@ -2585,7 +2581,7 @@ register_exception_impl(handle scope, const char *name, handle base, bool isLoca
try {
std::rethrow_exception(p);
} catch (const CppException &e) {
detail::get_exception_object<CppException>()(e.what());
set_error(detail::get_exception_object<CppException>(), e.what());
}
});
return ex;
Expand Down
8 changes: 8 additions & 0 deletions include/pybind11/pytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,14 @@ class handle : public detail::object_api<handle> {
#endif
};

inline void set_error(const handle &type, const char *message) {
PyErr_SetString(type.ptr(), message);
}

inline void set_error(const handle &type, const handle &value) {
PyErr_SetObject(type.ptr(), value.ptr());
}

/** \rst
Holds a reference to a Python object (with reference counting)
Expand Down
4 changes: 2 additions & 2 deletions tests/cross_module_interleaved_error_already_set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ namespace {
namespace py = pybind11;

void interleaved_error_already_set() {
PyErr_SetString(PyExc_RuntimeError, "1st error.");
py::set_error(PyExc_RuntimeError, "1st error.");
try {
throw py::error_already_set();
} catch (const py::error_already_set &) {
// The 2nd error could be conditional in a real application.
PyErr_SetString(PyExc_RuntimeError, "2nd error.");
py::set_error(PyExc_RuntimeError, "2nd error.");
} // Here the 1st error is destroyed before the 2nd error is fetched.
// The error_already_set dtor triggers a pybind11::detail::get_internals()
// call via pybind11::gil_scoped_acquire.
Expand Down
8 changes: 4 additions & 4 deletions tests/pybind11_cross_module_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) {
// test_exceptions.py
py::register_local_exception<LocalSimpleException>(m, "LocalSimpleException");
m.def("raise_runtime_error", []() {
PyErr_SetString(PyExc_RuntimeError, "My runtime error");
py::set_error(PyExc_RuntimeError, "My runtime error");
throw py::error_already_set();
});
m.def("raise_value_error", []() {
PyErr_SetString(PyExc_ValueError, "My value error");
py::set_error(PyExc_ValueError, "My value error");
throw py::error_already_set();
});
m.def("throw_pybind_value_error", []() { throw py::value_error("pybind11 value error"); });
Expand All @@ -49,7 +49,7 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) {
std::rethrow_exception(p);
}
} catch (const shared_exception &e) {
PyErr_SetString(PyExc_KeyError, e.what());
py::set_error(PyExc_KeyError, e.what());
}
});

Expand All @@ -60,7 +60,7 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) {
std::rethrow_exception(p);
}
} catch (const LocalException &e) {
PyErr_SetString(PyExc_KeyError, e.what());
py::set_error(PyExc_KeyError, e.what());
}
});

Expand Down
37 changes: 32 additions & 5 deletions tests/test_exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class MyException : public std::exception {
std::string message = "";
};

class MyExceptionUseDeprecatedOperatorCall : public MyException {
using MyException::MyException;
};

// A type that should be translated to a standard Python exception
class MyException2 : public std::exception {
public:
Expand Down Expand Up @@ -109,16 +113,34 @@ TEST_SUBMODULE(exceptions, m) {
m.def("throw_std_exception",
[]() { throw std::runtime_error("This exception was intentionally thrown."); });

// Please keep in sync with docs/advanced/exceptions.rst
static const auto *const ex = new py::exception<MyException>(m, "MyException");
// PLEASE KEEP IN SYNC with docs/advanced/exceptions.rst
static py::handle ex = py::exception<MyException>(m, "MyException").release();
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) {
std::rethrow_exception(p);
}
} catch (const MyException &e) {
// Set MyException as the active python error
(*ex)(e.what());
py::set_error(ex, e.what());
}
});

// Same as above, but using the deprecated `py::exception<>::operator()`
// We want to be sure it still works, until it's removed.
static const auto *const exd = new py::exception<MyExceptionUseDeprecatedOperatorCall>(
m, "MyExceptionUseDeprecatedOperatorCall");
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) {
std::rethrow_exception(p);
}
} catch (const MyExceptionUseDeprecatedOperatorCall &e) {
PYBIND11_WARNING_PUSH
PYBIND11_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
PYBIND11_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
(*exd)(e.what());
PYBIND11_WARNING_POP
}
});

Expand Down Expand Up @@ -166,7 +188,12 @@ TEST_SUBMODULE(exceptions, m) {
}
});

m.def("throws1", []() { throw MyException("this error should go to a custom type"); });
m.def("throws1",
[]() { throw MyException("this error should go to py::exception<MyException>"); });
m.def("throws1d", []() {
throw MyExceptionUseDeprecatedOperatorCall(
"this error should go to py::exception<MyExceptionUseDeprecatedOperatorCall>");
});
m.def("throws2",
[]() { throw MyException2("this error should go to a standard Python exception"); });
m.def("throws3", []() { throw MyException3("this error cannot be translated"); });
Expand Down Expand Up @@ -306,7 +333,7 @@ TEST_SUBMODULE(exceptions, m) {
});

m.def("error_already_set_what", [](const py::object &exc_type, const py::object &exc_value) {
PyErr_SetObject(exc_type.ptr(), exc_value.ptr());
py::set_error(exc_type, exc_value);
std::string what = py::error_already_set().what();
bool py_err_set_after_what = (PyErr_Occurred() != nullptr);
PyErr_Clear();
Expand Down
2 changes: 1 addition & 1 deletion tests/test_exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ class PYBIND11_EXPORT_EXCEPTION shared_exception : public pybind11::builtin_exce
public:
using builtin_exception::builtin_exception;
explicit shared_exception() : shared_exception("") {}
void set_error() const override { PyErr_SetString(PyExc_RuntimeError, what()); }
void set_error() const override { py::set_error(PyExc_RuntimeError, what()); }
};
10 changes: 9 additions & 1 deletion tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,15 @@ def test_custom(msg):
# Can we catch a MyException?
with pytest.raises(m.MyException) as excinfo:
m.throws1()
assert msg(excinfo.value) == "this error should go to a custom type"
assert msg(excinfo.value) == "this error should go to py::exception<MyException>"

# Can we catch a MyExceptionUseDeprecatedOperatorCall?
with pytest.raises(m.MyExceptionUseDeprecatedOperatorCall) as excinfo:
m.throws1d()
assert (
msg(excinfo.value)
== "this error should go to py::exception<MyExceptionUseDeprecatedOperatorCall>"
)

# Can we translate to standard Python exceptions?
with pytest.raises(RuntimeError) as excinfo:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_pytypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ PyObject *conv(PyObject *o) {
ret = PyFloat_FromDouble(v);
}
} else {
PyErr_SetString(PyExc_TypeError, "Unexpected type");
py::set_error(PyExc_TypeError, "Unexpected type");
}
return ret;
}
Expand Down
Loading

0 comments on commit daaabcc

Please sign in to comment.