diff --git a/docs/advanced.rst b/docs/advanced.rst index 1158f053e..07901e852 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -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(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 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 d523d0d9c..1468467be 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1281,7 +1281,7 @@ void register_exception_translator(ExceptionTranslator&& translator) { template 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(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 exception& register_exception(module &m, const std::string &name, PyObject* base = PyExc_Exception) { + static exception 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 613b1355f..ca2afa642 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -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(m, "MyException5"); + // A slightly more complicated one that declares MyException5_1 as a subclass of MyException5 + py::register_exception(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 4048d43f5..a9b4b0574 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -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"