diff --git a/cmake/darwin-ld-cpython.sym b/cmake/darwin-ld-cpython.sym index 8f2157df..9ffd11d0 100644 --- a/cmake/darwin-ld-cpython.sym +++ b/cmake/darwin-ld-cpython.sym @@ -132,6 +132,7 @@ -U _PyErr_FormatV -U _PyErr_GetExcInfo -U _PyErr_GetHandledException +-U _PyErr_GetRaisedException -U _PyErr_GivenExceptionMatches -U _PyErr_NewException -U _PyErr_NewExceptionWithDoc @@ -161,6 +162,7 @@ -U _PyErr_SetInterruptEx -U _PyErr_SetNone -U _PyErr_SetObject +-U _PyErr_SetRaisedException -U _PyErr_SetString -U _PyErr_SyntaxLocation -U _PyErr_SyntaxLocationEx diff --git a/docs/api_core.rst b/docs/api_core.rst index 91979b45..64c82fb6 100644 --- a/docs/api_core.rst +++ b/docs/api_core.rst @@ -1095,7 +1095,7 @@ the reference section on :ref:`class binding `. Returns a handle to the exception value - .. cpp:function:: handle traceback() const + .. cpp:function:: object traceback() const Returns a handle to the exception's traceback object diff --git a/include/nanobind/nb_error.h b/include/nanobind/nb_error.h index 1e860e1f..0023affb 100644 --- a/include/nanobind/nb_error.h +++ b/include/nanobind/nb_error.h @@ -10,11 +10,21 @@ NAMESPACE_BEGIN(NB_NAMESPACE) /// RAII wrapper that temporarily clears any Python error state +#if PY_VERSION_HEX >= 0x030C0000 +struct error_scope { + error_scope() { value = PyErr_GetRaisedException(); } + ~error_scope() { PyErr_SetRaisedException(value); } +private: + PyObject *value; +}; +#else struct error_scope { error_scope() { PyErr_Fetch(&type, &value, &trace); } ~error_scope() { PyErr_Restore(type, value, trace); } +private: PyObject *type, *value, *trace; }; +#endif /// Wraps a Python error state as a C++ exception class NB_EXPORT python_error : public std::exception { @@ -25,7 +35,11 @@ class NB_EXPORT python_error : public std::exception { NB_EXPORT_SHARED ~python_error() override; bool matches(handle exc) const noexcept { +#if PY_VERSION_HEX < 0x030C0000 return PyErr_GivenExceptionMatches(m_type, exc.ptr()) != 0; +#else + return PyErr_GivenExceptionMatches(m_value, exc.ptr()) != 0; +#endif } /// Move the error back into the Python domain. This may only be called @@ -42,16 +56,28 @@ class NB_EXPORT python_error : public std::exception { PyErr_WriteUnraisable(context.ptr()); } - handle type() const { return m_type; } handle value() const { return m_value; } - handle trace() const { return m_trace; } + +#if PY_VERSION_HEX < 0x030C0000 + handle type() const { return m_type; } + object traceback() const { return steal(m_traceback); } +#else + handle type() const { return value().type(); } + object traceback() const { return steal(PyException_GetTraceback(m_value)); } +#endif + [[deprecated]] + object trace() const { return traceback(); } NB_EXPORT_SHARED const char *what() const noexcept override; private: +#if PY_VERSION_HEX < 0x030C0000 mutable PyObject *m_type = nullptr; mutable PyObject *m_value = nullptr; - mutable PyObject *m_trace = nullptr; + mutable PyObject *m_traceback = nullptr; +#else + mutable PyObject *m_value = nullptr; +#endif mutable char *m_what = nullptr; }; diff --git a/src/error.cpp b/src/error.cpp index 579ad4f8..60d57cda 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -18,21 +18,67 @@ Buffer buf(128); NAMESPACE_END(detail) +#if PY_VERSION_HEX >= 0x030C0000 python_error::python_error() { - PyErr_Fetch(&m_type, &m_value, &m_trace); + m_value = PyErr_GetRaisedException(); + check(m_value, + "nanobind::python_error::python_error(): error indicator unset!"); +} + +python_error::~python_error() { + if (m_value) { + gil_scoped_acquire acq; + /* With GIL held */ { + // Clear error status in case the following executes Python code + error_scope scope; + Py_DECREF(m_value); + } + } + + free(m_what); +} + +python_error::python_error(const python_error &e) + : std::exception(e), m_value(e.m_value) { + if (m_value) { + gil_scoped_acquire acq; + Py_INCREF(m_value); + } + if (e.m_what) + m_what = NB_STRDUP(e.m_what); +} + +python_error::python_error(python_error &&e) noexcept + : std::exception(e), m_value(e.m_value), m_what(e.m_what) { + e.m_value = nullptr; + e.m_what = nullptr; +} + +void python_error::restore() noexcept { + check(m_value, + "nanobind::python_error::restore(): error was already restored!"); + + PyErr_SetRaisedException(m_value); + m_value = nullptr; +} + +#else /* Exception handling for Python 3.11 and older versions */ + +python_error::python_error() { + PyErr_Fetch(&m_type, &m_value, &m_traceback); check(m_type, "nanobind::python_error::python_error(): error indicator unset!"); } python_error::~python_error() { - if (m_type || m_value || m_trace) { + if (m_type) { gil_scoped_acquire acq; /* With GIL held */ { // Clear error status in case the following executes Python code error_scope scope; - Py_XDECREF(m_type); Py_XDECREF(m_value); - Py_XDECREF(m_trace); + Py_XDECREF(m_type); + Py_XDECREF(m_traceback); } } free(m_what); @@ -40,12 +86,12 @@ python_error::~python_error() { python_error::python_error(const python_error &e) : std::exception(e), m_type(e.m_type), m_value(e.m_value), - m_trace(e.m_trace) { - if (m_type || m_value || m_trace) { + m_traceback(e.m_traceback) { + if (m_type) { gil_scoped_acquire acq; - Py_XINCREF(m_type); + Py_INCREF(m_type); Py_XINCREF(m_value); - Py_XINCREF(m_trace); + Py_XINCREF(m_traceback); } if (e.m_what) m_what = NB_STRDUP(e.m_what); @@ -53,11 +99,21 @@ python_error::python_error(const python_error &e) python_error::python_error(python_error &&e) noexcept : std::exception(e), m_type(e.m_type), m_value(e.m_value), - m_trace(e.m_trace), m_what(e.m_what) { - e.m_type = e.m_value = e.m_trace = nullptr; + m_traceback(e.m_traceback), m_what(e.m_what) { + e.m_type = e.m_value = e.m_traceback = nullptr; e.m_what = nullptr; } +void python_error::restore() noexcept { + check(m_type, + "nanobind::python_error::restore(): error was already restored!"); + + PyErr_Restore(m_type, m_value, m_traceback); + m_type = m_value = m_traceback = nullptr; +} + +#endif + const char *python_error::what() const noexcept { using detail::buf; @@ -71,23 +127,30 @@ const char *python_error::what() const noexcept { if (m_what) return m_what; - PyErr_NormalizeException(&m_type, &m_value, &m_trace); +#if PY_VERSION_HEX < 0x030C0000 + PyErr_NormalizeException(&m_type, &m_value, &m_traceback); check(m_type, "nanobind::python_error::what(): PyNormalize_Exception() failed!"); - if (m_trace) { - if (PyException_SetTraceback(m_value, m_trace) < 0) + if (m_traceback) { + if (PyException_SetTraceback(m_value, m_traceback) < 0) PyErr_Clear(); } + handle exc_type = m_type, exc_value = m_value; +#else + handle exc_value = m_value, exc_type = exc_value.type(); +#endif + object exc_traceback = traceback(); + #if defined(Py_LIMITED_API) || defined(PYPY_VERSION) object mod = module_::import_("traceback"), - result = mod.attr("format_exception")(handle(m_type), handle(m_value), handle(m_trace)); + result = mod.attr("format_exception")(exc_type, exc_value, exc_traceback); m_what = NB_STRDUP(borrow(str("\n").attr("join")(result)).c_str()); #else buf.clear(); - if (m_trace) { - PyTracebackObject *to = (PyTracebackObject *) m_trace; + if (exc_traceback.is_valid()) { + PyTracebackObject *to = (PyTracebackObject *) exc_traceback.ptr(); // Get the deepest trace possible while (to->tb_next) @@ -130,13 +193,13 @@ const char *python_error::what() const noexcept { } } - if (m_type) { - object name = handle(m_type).attr("__name__"); + if (exc_type.is_valid()) { + object name = exc_type.attr("__name__"); buf.put_dstr(borrow(name).c_str()); buf.put(": "); } - if (m_value) + if (exc_value.is_valid()) buf.put_dstr(str(m_value).c_str()); m_what = buf.copy(); #endif @@ -144,14 +207,6 @@ const char *python_error::what() const noexcept { return m_what; } -void python_error::restore() noexcept { - check(m_type, - "nanobind::python_error::restore(): error was already restored!"); - - PyErr_Restore(m_type, m_value, m_trace); - m_type = m_value = m_trace = nullptr; -} - builtin_exception::builtin_exception(exception_type type, const char *what) : std::runtime_error(what ? what : ""), m_type(type) { } builtin_exception::~builtin_exception() { }