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.
diff --git a/docs/advanced.rst b/docs/advanced.rst
index 1158f05..07901e8 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -1228,56 +1228,82 @@
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:
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index d523d0d..1468467 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -1281,7 +1281,7 @@
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 @@
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());
diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp
index 613b135..ca2afa6 100644
--- a/tests/test_exceptions.cpp
+++ b/tests/test_exceptions.cpp
@@ -46,6 +46,18 @@
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 @@
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 @@
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 @@
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 @@
}
});
+ // 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) {
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 4048d43..a9b4b05 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -22,7 +22,8 @@
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 @@
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"