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
exception translators.
The function ``register_exception_translator(translator)`` takes a stateless
callable (e.g. a function pointer or a lambda function without captured
variables) with the following call signature: ``void(std::exception_ptr)``.
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``:
To register a simple exception conversion that translates a C++ exception into
a new Python exception using the C++ exception's ``what()`` method, a helper
function is available:
.. 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) {
try {
if (p) std::rethrow_exception(p);
} catch (const MyCustomException &e) {
exc(e.what());
} catch (const OtherException &e) {
PyErr_SetString(PyExc_RuntimeError, e.what());
}
});
Multiple exceptions can be handled by a single translator. If the exception is
not caught by the current translator, the previously registered one gets a
chance.
Multiple exceptions can be handled by a single translator, as shown in the
example above. If the exception is not caught by the current translator, the
previously registered one gets a chance.
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
section.
.. seealso::
The file :file:`tests/test_exceptions.cpp` contains examples
of various custom exception translators and custom exception types.
.. note::
You must either call ``PyErr_SetString`` 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``.
You must call either ``PyErr_SetString`` or a custom exception's call
operator (``exc(string)``) 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``.
Exceptions that you do not plan to handle should simply not be caught.
You may also choose to explicity (re-)throw the exception to delegate it to
the other existing exception translators.
The ``py::exception`` wrapper for creating custom exceptions cannot (yet)
be used as a base type.
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,
previously-declared existing exception translators.
.. _eigen:

View File

@ -1281,7 +1281,7 @@ void register_exception_translator(ExceptionTranslator&& translator) {
template <typename type>
class exception : public object {
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(".") + name;
char* exception_name = const_cast<char*>(full_name.c_str());
@ -1289,8 +1289,32 @@ public:
inc_ref(); // PyModule_AddObject() steals a reference
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)
PYBIND11_NOINLINE inline void print(tuple args, dict kwargs) {
auto strings = tuple(args.size());

View File

@ -46,6 +46,18 @@ private:
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() {
throw MyException("this error should go to a custom type");
}
@ -62,6 +74,14 @@ void throws4() {
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() {
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 {
if (p) std::rethrow_exception(p);
} 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 {
if (p) std::rethrow_exception(p);
} catch (const MyException2 &e) {
// Translate this exception to a standard RuntimeError
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("throws2", &throws2);
m.def("throws3", &throws3);
m.def("throws4", &throws4);
m.def("throws5", &throws5);
m.def("throws5_1", &throws5_1);
m.def("throws_logic_error", &throws_logic_error);
m.def("throw_already_set", [](bool err) {

View File

@ -22,7 +22,8 @@ def test_python_call_in_catch():
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)
# Can we catch a MyException?"
@ -49,3 +50,25 @@ def test_custom(msg):
with pytest.raises(RuntimeError) as excinfo:
throws_logic_error()
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"