Added py::register_exception for simple case (#296)

The custom exception handling added in PR #273 is robust, but is overly
complex for declaring the most common simple C++ -> Python exception
mapping that needs only to copy `what()`.  This add a simpler
`py::register_exception<CppExp>(module, "PyExp");` function that greatly
simplifies the common basic case of translation of a simple CppException
into a simple PythonException, while not removing the more advanced
capabilities of defining custom exception handlers.
This commit is contained in:
Jason Rhinelander 2016-09-16 02:04:15 -04:00 committed by Wenzel Jakob
parent 29b5064e9c
commit b3794f1087
4 changed files with 136 additions and 34 deletions

View File

@ -1228,56 +1228,82 @@ If the default exception conversion policy described
is insufficient, pybind11 also provides support for registering custom is insufficient, pybind11 also provides support for registering custom
exception translators. exception translators.
The function ``register_exception_translator(translator)`` takes a stateless To register a simple exception conversion that translates a C++ exception into
callable (e.g. a function pointer or a lambda function without captured a new Python exception using the C++ exception's ``what()`` method, a helper
variables) with the following call signature: ``void(std::exception_ptr)``. function is available:
When a C++ exception is thrown, registered exception translators are tried
in reverse order of registration (i.e. the last registered translator gets
a first shot at handling the exception).
Inside the translator, ``std::rethrow_exception`` should be used within
a try block to re-throw the exception. A catch clause can then use
``PyErr_SetString`` to set a Python exception as demonstrated
in :file:`tests/test_exceptions.cpp`.
This example also demonstrates how to create custom exception types
with ``py::exception``.
The following example demonstrates this for a hypothetical exception class
``MyCustomException``:
.. code-block:: cpp .. code-block:: cpp
py::register_exception<CppExp>(module, "PyExp");
This call creates a Python exception class with the name ``PyExp`` in the given
module and automatically converts any encountered exceptions of type ``CppExp``
into Python exceptions of type ``PyExp``.
When more advanced exception translation is needed, the function
``py::register_exception_translator(translator)`` can be used to register
functions that can translate arbitrary exception types (and which may include
additional logic to do so). The function takes a stateless callable (e.g. a
function pointer or a lambda function without captured variables) with the call
signature ``void(std::exception_ptr)``.
When a C++ exception is thrown, the registered exception translators are tried
in reverse order of registration (i.e. the last registered translator gets the
first shot at handling the exception).
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).
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
to make this a static declaration when using it inside a lambda expression
without requiring capturing).
The following example demonstrates this for a hypothetical exception classes
``MyCustomException`` and ``OtherException``: the first is translated to a
custom python exception ``MyCustomError``, while the second is translated to a
standard python RuntimeError:
.. code-block:: cpp
static py::exception<MyCustomException> exc(m, "MyCustomError");
py::register_exception_translator([](std::exception_ptr p) { py::register_exception_translator([](std::exception_ptr p) {
try { try {
if (p) std::rethrow_exception(p); if (p) std::rethrow_exception(p);
} catch (const MyCustomException &e) { } catch (const MyCustomException &e) {
exc(e.what());
} catch (const OtherException &e) {
PyErr_SetString(PyExc_RuntimeError, e.what()); PyErr_SetString(PyExc_RuntimeError, e.what());
} }
}); });
Multiple exceptions can be handled by a single translator. If the exception is Multiple exceptions can be handled by a single translator, as shown in the
not caught by the current translator, the previously registered one gets a example above. If the exception is not caught by the current translator, the
chance. previously registered one gets a chance.
If none of the registered exception translators is able to handle the If none of the registered exception translators is able to handle the
exception, it is handled by the default converter as described in the previous exception, it is handled by the default converter as described in the previous
section. section.
.. seealso::
The file :file:`tests/test_exceptions.cpp` contains examples
of various custom exception translators and custom exception types.
.. note:: .. note::
You must either call ``PyErr_SetString`` for every exception caught in a You must call either ``PyErr_SetString`` or a custom exception's call
custom exception translator. Failure to do so will cause Python to crash operator (``exc(string)``) for every exception caught in a custom exception
with ``SystemError: error return without exception set``. translator. Failure to do so will cause Python to crash with ``SystemError:
error return without exception set``.
Exceptions that you do not plan to handle should simply not be caught. Exceptions that you do not plan to handle should simply not be caught, or
may be explicity (re-)thrown to delegate it to the other,
You may also choose to explicity (re-)throw the exception to delegate it to previously-declared existing exception translators.
the other existing exception translators.
The ``py::exception`` wrapper for creating custom exceptions cannot (yet)
be used as a base type.
.. _eigen: .. _eigen:

View File

@ -1281,7 +1281,7 @@ void register_exception_translator(ExceptionTranslator&& translator) {
template <typename type> template <typename type>
class exception : public object { class exception : public object {
public: public:
exception(module &m, const std::string name, PyObject* base=PyExc_Exception) { exception(module &m, const std::string &name, PyObject* base=PyExc_Exception) {
std::string full_name = std::string(PyModule_GetName(m.ptr())) std::string full_name = std::string(PyModule_GetName(m.ptr()))
+ std::string(".") + name; + std::string(".") + name;
char* exception_name = const_cast<char*>(full_name.c_str()); char* exception_name = const_cast<char*>(full_name.c_str());
@ -1289,8 +1289,32 @@ public:
inc_ref(); // PyModule_AddObject() steals a reference inc_ref(); // PyModule_AddObject() steals a reference
PyModule_AddObject(m.ptr(), name.c_str(), m_ptr); PyModule_AddObject(m.ptr(), name.c_str(), m_ptr);
} }
// Sets the current python exception to this exception object with the given message
void operator()(const char *message) {
PyErr_SetString(m_ptr, message);
}
}; };
/** Registers a Python exception in `m` of the given `name` and installs an exception translator to
* translate the C++ exception to the created Python exception using the exceptions what() method.
* This is intended for simple exception translations; for more complex translation, register the
* exception object and translator directly.
*/
template <typename CppException> exception<CppException>& register_exception(module &m, const std::string &name, PyObject* base = PyExc_Exception) {
static exception<CppException> ex(m, name, base);
register_exception_translator([](std::exception_ptr p) {
if (!p) return;
try {
std::rethrow_exception(p);
}
catch (const CppException &e) {
ex(e.what());
}
});
return ex;
}
NAMESPACE_BEGIN(detail) NAMESPACE_BEGIN(detail)
PYBIND11_NOINLINE inline void print(tuple args, dict kwargs) { PYBIND11_NOINLINE inline void print(tuple args, dict kwargs) {
auto strings = tuple(args.size()); auto strings = tuple(args.size());

View File

@ -46,6 +46,18 @@ private:
std::string message = ""; std::string message = "";
}; };
// Like the above, but declared via the helper function
class MyException5 : public std::logic_error {
public:
explicit MyException5(const std::string &what) : std::logic_error(what) {}
};
// Inherits from MyException5
class MyException5_1 : public MyException5 {
using MyException5::MyException5;
};
void throws1() { void throws1() {
throw MyException("this error should go to a custom type"); throw MyException("this error should go to a custom type");
} }
@ -62,6 +74,14 @@ void throws4() {
throw MyException4("this error is rethrown"); throw MyException4("this error is rethrown");
} }
void throws5() {
throw MyException5("this is a helper-defined translated exception");
}
void throws5_1() {
throw MyException5_1("MyException5 subclass");
}
void throws_logic_error() { void throws_logic_error() {
throw std::logic_error("this error should fall through to the standard handler"); throw std::logic_error("this error should fall through to the standard handler");
} }
@ -80,7 +100,8 @@ test_initializer custom_exceptions([](py::module &m) {
try { try {
if (p) std::rethrow_exception(p); if (p) std::rethrow_exception(p);
} catch (const MyException &e) { } catch (const MyException &e) {
PyErr_SetString(ex.ptr(), e.what()); // Set MyException as the active python error
ex(e.what());
} }
}); });
@ -91,6 +112,7 @@ test_initializer custom_exceptions([](py::module &m) {
try { try {
if (p) std::rethrow_exception(p); if (p) std::rethrow_exception(p);
} catch (const MyException2 &e) { } catch (const MyException2 &e) {
// Translate this exception to a standard RuntimeError
PyErr_SetString(PyExc_RuntimeError, e.what()); PyErr_SetString(PyExc_RuntimeError, e.what());
} }
}); });
@ -106,10 +128,17 @@ test_initializer custom_exceptions([](py::module &m) {
} }
}); });
// A simple exception translation:
auto ex5 = py::register_exception<MyException5>(m, "MyException5");
// A slightly more complicated one that declares MyException5_1 as a subclass of MyException5
py::register_exception<MyException5_1>(m, "MyException5_1", ex5.ptr());
m.def("throws1", &throws1); m.def("throws1", &throws1);
m.def("throws2", &throws2); m.def("throws2", &throws2);
m.def("throws3", &throws3); m.def("throws3", &throws3);
m.def("throws4", &throws4); m.def("throws4", &throws4);
m.def("throws5", &throws5);
m.def("throws5_1", &throws5_1);
m.def("throws_logic_error", &throws_logic_error); m.def("throws_logic_error", &throws_logic_error);
m.def("throw_already_set", [](bool err) { m.def("throw_already_set", [](bool err) {

View File

@ -22,7 +22,8 @@ def test_python_call_in_catch():
def test_custom(msg): def test_custom(msg):
from pybind11_tests import (MyException, throws1, throws2, throws3, throws4, from pybind11_tests import (MyException, MyException5, MyException5_1,
throws1, throws2, throws3, throws4, throws5, throws5_1,
throws_logic_error) throws_logic_error)
# Can we catch a MyException?" # Can we catch a MyException?"
@ -49,3 +50,25 @@ def test_custom(msg):
with pytest.raises(RuntimeError) as excinfo: with pytest.raises(RuntimeError) as excinfo:
throws_logic_error() throws_logic_error()
assert msg(excinfo.value) == "this error should fall through to the standard handler" assert msg(excinfo.value) == "this error should fall through to the standard handler"
# Can we handle a helper-declared exception?
with pytest.raises(MyException5) as excinfo:
throws5()
assert msg(excinfo.value) == "this is a helper-defined translated exception"
# Exception subclassing:
with pytest.raises(MyException5) as excinfo:
throws5_1()
assert msg(excinfo.value) == "MyException5 subclass"
assert isinstance(excinfo.value, MyException5_1)
with pytest.raises(MyException5_1) as excinfo:
throws5_1()
assert msg(excinfo.value) == "MyException5 subclass"
with pytest.raises(MyException5) as excinfo:
try:
throws5()
except MyException5_1 as e:
raise RuntimeError("Exception error: caught child from parent")
assert msg(excinfo.value) == "this is a helper-defined translated exception"