Add support for user defined exception translators

This commit is contained in:
Pim Schellart 2016-06-17 17:35:59 -04:00
parent 678d59d21f
commit 5a7d17ff16
9 changed files with 302 additions and 12 deletions

View File

@ -817,6 +817,8 @@ In other words, :func:`init` creates an anonymous function that invokes an
in-place constructor. Memory allocation etc. is already take care of beforehand in-place constructor. Memory allocation etc. is already take care of beforehand
within pybind11. within pybind11.
.. _catching_and_throwing_exceptions:
Catching and throwing exceptions Catching and throwing exceptions
================================ ================================
@ -869,6 +871,65 @@ There is also a special exception :class:`cast_error` that is thrown by
:func:`handle::call` when the input arguments cannot be converted to Python :func:`handle::call` when the input arguments cannot be converted to Python
objects. objects.
Registering custom exception translators
========================================
If the default exception conversion policy described
:ref:`above <catching_and_throwing_exceptions>`
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:`example19.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
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) std::rethrow_exception(p);
} catch (const MyCustomException &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.
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.
.. 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``.
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 ``py::base``.
.. _opaque: .. _opaque:
Treating STL data structures as opaque objects Treating STL data structures as opaque objects

View File

@ -25,6 +25,7 @@ set(PYBIND11_EXAMPLES
example16.cpp example16.cpp
example17.cpp example17.cpp
example18.cpp example18.cpp
example19.cpp
issues.cpp issues.cpp
) )

View File

@ -27,6 +27,7 @@ void init_ex15(py::module &);
void init_ex16(py::module &); void init_ex16(py::module &);
void init_ex17(py::module &); void init_ex17(py::module &);
void init_ex18(py::module &); void init_ex18(py::module &);
void init_ex19(py::module &);
void init_issues(py::module &); void init_issues(py::module &);
#if defined(PYBIND11_TEST_EIGEN) #if defined(PYBIND11_TEST_EIGEN)
@ -54,6 +55,7 @@ PYBIND11_PLUGIN(example) {
init_ex16(m); init_ex16(m);
init_ex17(m); init_ex17(m);
init_ex18(m); init_ex18(m);
init_ex19(m);
init_issues(m); init_issues(m);
#if defined(PYBIND11_TEST_EIGEN) #if defined(PYBIND11_TEST_EIGEN)

108
example/example19.cpp Normal file
View File

@ -0,0 +1,108 @@
/*
example/example19.cpp -- exception translation
Copyright (c) 2016 Pim Schellart <P.Schellart@princeton.edu>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#include "example.h"
// A type that should be raised as an exeption in Python
class MyException : public std::exception {
public:
explicit MyException(const char * m) : message{m} {}
virtual const char * what() const noexcept override {return message.c_str();}
private:
std::string message = "";
};
// A type that should be translated to a standard Python exception
class MyException2 : public std::exception {
public:
explicit MyException2(const char * m) : message{m} {}
virtual const char * what() const noexcept override {return message.c_str();}
private:
std::string message = "";
};
// A type that is not derived from std::exception (and is thus unknown)
class MyException3 {
public:
explicit MyException3(const char * m) : message{m} {}
virtual const char * what() const noexcept {return message.c_str();}
private:
std::string message = "";
};
// A type that should be translated to MyException
// and delegated to its exception translator
class MyException4 : public std::exception {
public:
explicit MyException4(const char * m) : message{m} {}
virtual const char * what() const noexcept override {return message.c_str();}
private:
std::string message = "";
};
void throws1() {
throw MyException("this error should go to a custom type");
}
void throws2() {
throw MyException2("this error should go to a standard Python exception");
}
void throws3() {
throw MyException3("this error cannot be translated");
}
void throws4() {
throw MyException4("this error is rethrown");
}
void throws_logic_error() {
throw std::logic_error("this error should fall through to the standard handler");
}
void init_ex19(py::module &m) {
// make a new custom exception and use it as a translation target
static py::exception<MyException> ex(m, "MyException");
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) std::rethrow_exception(p);
} catch (const MyException &e) {
PyErr_SetString(ex.ptr(), e.what());
}
});
// register new translator for MyException2
// no need to store anything here because this type will
// never by visible from Python
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) std::rethrow_exception(p);
} catch (const MyException2 &e) {
PyErr_SetString(PyExc_RuntimeError, e.what());
}
});
// register new translator for MyException4
// which will catch it and delegate to the previously registered
// translator for MyException by throwing a new exception
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) std::rethrow_exception(p);
} catch (const MyException4 &e) {
throw MyException(e.what());
}
});
m.def("throws1", &throws1);
m.def("throws2", &throws2);
m.def("throws3", &throws3);
m.def("throws4", &throws4);
m.def("throws_logic_error", &throws_logic_error);
}

42
example/example19.py Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env python
from __future__ import print_function
import sys
sys.path.append('.')
import example
print("Can we catch a MyException?")
try:
example.throws1()
except example.MyException as e:
print(e.__class__.__name__, ":", e)
print("")
print("Can we translate to standard Python exceptions?")
try:
example.throws2()
except Exception as e:
print(e.__class__.__name__, ":", e)
print("")
print("Can we handle unknown exceptions?")
try:
example.throws3()
except Exception as e:
print(e.__class__.__name__, ":", e)
print("")
print("Can we delegate to another handler by rethrowing?")
try:
example.throws4()
except example.MyException as e:
print(e.__class__.__name__, ":", e)
print("")
print("Can we fall-through to the default handler?")
try:
example.throws_logic_error()
except Exception as e:
print(e.__class__.__name__, ":", e)
print("")

15
example/example19.ref Normal file
View File

@ -0,0 +1,15 @@
Can we catch a MyException?
MyException : this error should go to a custom type
Can we translate to standard Python exceptions?
RuntimeError : this error should go to a standard Python exception
Can we handle unknown exceptions?
RuntimeError : Caught an unknown exception!
Can we delegate to another handler by rethrowing?
MyException : this error is rethrown
Can we fall-through to the default handler?
RuntimeError : this error should fall through to the standard handler

View File

@ -49,6 +49,27 @@ PYBIND11_NOINLINE inline internals &get_internals() {
internals_ptr->istate = tstate->interp; internals_ptr->istate = tstate->interp;
#endif #endif
builtins[id] = capsule(internals_ptr); builtins[id] = capsule(internals_ptr);
internals_ptr->registered_exception_translators.push_front(
[](std::exception_ptr p) -> void {
try {
if (p) std::rethrow_exception(p);
} catch (const error_already_set &) { return;
} catch (const index_error &e) { PyErr_SetString(PyExc_IndexError, e.what()); return;
} catch (const value_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
} catch (const stop_iteration &e) { PyErr_SetString(PyExc_StopIteration, e.what()); return;
} catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return;
} catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
} catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
} catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
} catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return;
} catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
} catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return;
} catch (...) {
PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!");
return;
}
}
);
} }
return *internals_ptr; return *internals_ptr;
} }

View File

@ -66,6 +66,7 @@
# pragma warning(pop) # pragma warning(pop)
#endif #endif
#include <forward_list>
#include <vector> #include <vector>
#include <string> #include <string>
#include <stdexcept> #include <stdexcept>
@ -264,6 +265,7 @@ struct internals {
std::unordered_map<const void *, void*> registered_types_py; // PyTypeObject* -> type_info std::unordered_map<const void *, void*> registered_types_py; // PyTypeObject* -> type_info
std::unordered_map<const void *, void*> registered_instances; // void * -> PyObject* std::unordered_map<const void *, void*> registered_instances; // void * -> PyObject*
std::unordered_set<std::pair<const PyObject *, const char *>, overload_hash> inactive_overload_cache; std::unordered_set<std::pair<const PyObject *, const char *>, overload_hash> inactive_overload_cache;
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
#if defined(WITH_THREAD) #if defined(WITH_THREAD)
decltype(PyThread_create_key()) tstate = 0; // Usually an int but a long on Cygwin64 with Python 3.x decltype(PyThread_create_key()) tstate = 0; // Usually an int but a long on Cygwin64 with Python 3.x
PyInterpreterState *istate = nullptr; PyInterpreterState *istate = nullptr;

View File

@ -401,19 +401,32 @@ protected:
if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD) if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD)
break; break;
} }
} catch (const error_already_set &) { return nullptr; } catch (const error_already_set &) {
} catch (const index_error &e) { PyErr_SetString(PyExc_IndexError, e.what()); return nullptr; return nullptr;
} catch (const value_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
} catch (const stop_iteration &e) { PyErr_SetString(PyExc_StopIteration, e.what()); return nullptr;
} catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return nullptr;
} catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
} catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
} catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
} catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return nullptr;
} catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
} catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return nullptr;
} catch (...) { } catch (...) {
PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!"); /* When an exception is caught, give each registered exception
translator a chance to translate it to a Python exception
in reverse order of registration.
A translator may choose to do one of the following:
- catch the exception and call PyErr_SetString or PyErr_SetObject
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. */
auto last_exception = std::current_exception();
auto &registered_exception_translators = pybind11::detail::get_internals().registered_exception_translators;
for (auto& translator : registered_exception_translators) {
try {
translator(last_exception);
} catch (...) {
last_exception = std::current_exception();
continue;
}
return nullptr;
}
PyErr_SetString(PyExc_SystemError, "Exception escaped from default exception translator!");
return nullptr; return nullptr;
} }
@ -1089,6 +1102,31 @@ template <typename InputType, typename OutputType> void implicitly_convertible()
((detail::type_info *) it->second)->implicit_conversions.push_back(implicit_caster); ((detail::type_info *) it->second)->implicit_conversions.push_back(implicit_caster);
} }
template <typename ExceptionTranslator>
void register_exception_translator(ExceptionTranslator&& translator) {
detail::get_internals().registered_exception_translators.push_front(
std::forward<ExceptionTranslator>(translator));
}
/* Wrapper to generate a new Python exception type.
*
* This should only be used with PyErr_SetString for now.
* It is not (yet) possible to use as a py::base.
* Template type argument is reserved for future use.
*/
template <typename type>
class exception : public object {
public:
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());
m_ptr = PyErr_NewException(exception_name, base, NULL);
inc_ref(); // PyModule_AddObject() steals a reference
PyModule_AddObject(m.ptr(), name.c_str(), m_ptr);
}
};
#if defined(WITH_THREAD) #if defined(WITH_THREAD)
/* The functions below essentially reproduce the PyGILState_* API using a RAII /* The functions below essentially reproduce the PyGILState_* API using a RAII