From dc2d5a00abf875be910bde5603f8d274529b3f72 Mon Sep 17 00:00:00 2001 From: Galen Lynch Date: Wed, 31 Oct 2018 17:00:50 -0400 Subject: [PATCH 1/4] Make Julia exceptions wrapped in Python Exceptions more informative I have added a backtrace to the description of exceptions coming from Julia that are wrapped in Python exceptions. This makes it much easier to find the source of errors in Julia code being called from Python functions. --- src/exception.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exception.jl b/src/exception.jl index 8e706600..58c74fa4 100644 --- a/src/exception.jl +++ b/src/exception.jl @@ -132,7 +132,7 @@ be called via Python C-API, it tries to not throw at all cost. function showerror_string(e::T) where {T} try io = IOBuffer() - showerror(io, e) + showerror(io, e, catch_backtrace()) return String(take!(io)) catch try From fdae2be60de89cb4c07eeb46b0e392360173db5b Mon Sep 17 00:00:00 2001 From: Galen Lynch Date: Sun, 4 Nov 2018 16:28:04 -0500 Subject: [PATCH 2/4] Allow `pyraise` to include backtrace from Julia Instead of using `catch_backtrace` in `showerror_string`, which may not always be called from a catch block with a current exception set, I have added an optional argument to `pyraise` which can be used to pass backtraces to `showerror_string`. Backtraces are only used if exception is not a PyError exception. By default, no backtrace is shown. All try-catch blocks in PyCall attempt to pass the backtrace for the current exception to `pyraise`. --- src/callback.jl | 2 +- src/exception.jl | 16 +++++++++++----- src/pyiterator.jl | 6 +++--- src/pytype.jl | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/callback.jl b/src/callback.jl index 55d84936..43101edd 100644 --- a/src/callback.jl +++ b/src/callback.jl @@ -39,7 +39,7 @@ function _pyjlwrap_call(f, args_::PyPtr, kw_::PyPtr) return pyreturn(ret) catch e - pyraise(e) + pyraise(e, catch_backtrace()) finally args.o = PyPtr_NULL # don't decref end diff --git a/src/exception.jl b/src/exception.jl index 58c74fa4..84994361 100644 --- a/src/exception.jl +++ b/src/exception.jl @@ -123,16 +123,21 @@ function pyexc_initialize() pyexc[PyIOError] = @pyglobalobjptr :PyExc_IOError end +_showerror_string(io::IO, e, ::Nothing) = showerror(io, e) +_showerror_string(io::IO, e, bt) = showerror(io, e, bt) + +# bt argument defaults to nothing, to delay dispatching on the presence of a +# backtrace until after the try-catch block """ showerror_string(e) :: String Convert output of `showerror` to a `String`. Since this function may be called via Python C-API, it tries to not throw at all cost. """ -function showerror_string(e::T) where {T} +function showerror_string(e::T, bt = nothing) where {T} try io = IOBuffer() - showerror(io, e, catch_backtrace()) + _showerror_string(io, e, bt) return String(take!(io)) catch try @@ -163,14 +168,15 @@ function showerror_string(e::T) where {T} end end -function pyraise(e) +function pyraise(e, bt = nothing) eT = typeof(e) pyeT = haskey(pyexc::Dict, eT) ? pyexc[eT] : pyexc[Exception] ccall((@pysym :PyErr_SetString), Cvoid, (PyPtr, Cstring), - pyeT, string("Julia exception: ", showerror_string(e))) + pyeT, string("Julia exception: ", showerror_string(e, bt))) end -function pyraise(e::PyError) +# Second argument allows for backtraces passed to `pyraise` to be ignored. +function pyraise(e::PyError, ::Vector = []) ccall((@pysym :PyErr_Restore), Cvoid, (PyPtr, PyPtr, PyPtr), e.T, e.val, e.traceback) e.T.o = e.val.o = e.traceback.o = C_NULL # refs were stolen diff --git a/src/pyiterator.jl b/src/pyiterator.jl index 5b4e44d6..80b36987 100644 --- a/src/pyiterator.jl +++ b/src/pyiterator.jl @@ -134,7 +134,7 @@ const jlWrapIteratorType = PyTypeObject() return pyreturn(item) end catch e - pyraise(e) + pyraise(e, catch_backtrace()) end return PyPtr_NULL end @@ -149,7 +149,7 @@ else return pyreturn(item) end catch e - pyraise(e) + pyraise(e, catch_backtrace()) end return PyPtr_NULL end @@ -162,7 +162,7 @@ function pyjlwrap_getiter(self_::PyPtr) self = unsafe_pyjlwrap_to_objref(self_) return pystealref!(jlwrap_iterator(self)) catch e - pyraise(e) + pyraise(e, catch_backtrace()) end return PyPtr_NULL end diff --git a/src/pytype.jl b/src/pytype.jl index 387bc56f..e59303e2 100644 --- a/src/pytype.jl +++ b/src/pytype.jl @@ -348,7 +348,7 @@ function pyjlwrap_repr(o::PyPtr) return pyreturn(o != C_NULL ? string("") : "") catch e - pyraise(e) + pyraise(e, catch_backtrace()) return PyPtr_NULL end end @@ -395,7 +395,7 @@ function pyjlwrap_getattr(self_::PyPtr, attr__::PyPtr) end end catch e - pyraise(e) + pyraise(e, catch_backtrace()) finally attr_.o = PyPtr_NULL # don't decref end From 21cb46b9814879468b32e554ba9117dfc48efdb7 Mon Sep 17 00:00:00 2001 From: Galen Lynch Date: Mon, 5 Nov 2018 13:29:05 -0500 Subject: [PATCH 3/4] Add pyraise macro Added a pyraise macro, as written by @stevengj, which catches backtraces and calls the pyraise function. This macro should only be used in a catch block. --- src/callback.jl | 2 +- src/exception.jl | 10 ++++++++++ src/pyiterator.jl | 6 +++--- src/pytype.jl | 4 ++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/callback.jl b/src/callback.jl index 43101edd..332ba9c0 100644 --- a/src/callback.jl +++ b/src/callback.jl @@ -39,7 +39,7 @@ function _pyjlwrap_call(f, args_::PyPtr, kw_::PyPtr) return pyreturn(ret) catch e - pyraise(e, catch_backtrace()) + @pyraise e finally args.o = PyPtr_NULL # don't decref end diff --git a/src/exception.jl b/src/exception.jl index 84994361..ffb7c51d 100644 --- a/src/exception.jl +++ b/src/exception.jl @@ -181,3 +181,13 @@ function pyraise(e::PyError, ::Vector = []) e.T, e.val, e.traceback) e.T.o = e.val.o = e.traceback.o = C_NULL # refs were stolen end + +""" + @pyraise e + +Throw the exception `e` to Python, attaching a backtrace. This macro should only be +used in a `catch` block so that `catch_backtrace()` is valid. +""" +macro pyraise(e) + :(pyraise($(esc(e)), catch_backtrace())) +end diff --git a/src/pyiterator.jl b/src/pyiterator.jl index 80b36987..8cfbc1c9 100644 --- a/src/pyiterator.jl +++ b/src/pyiterator.jl @@ -134,7 +134,7 @@ const jlWrapIteratorType = PyTypeObject() return pyreturn(item) end catch e - pyraise(e, catch_backtrace()) + @pyraise e end return PyPtr_NULL end @@ -149,7 +149,7 @@ else return pyreturn(item) end catch e - pyraise(e, catch_backtrace()) + @pyraise e end return PyPtr_NULL end @@ -162,7 +162,7 @@ function pyjlwrap_getiter(self_::PyPtr) self = unsafe_pyjlwrap_to_objref(self_) return pystealref!(jlwrap_iterator(self)) catch e - pyraise(e, catch_backtrace()) + @pyraise e end return PyPtr_NULL end diff --git a/src/pytype.jl b/src/pytype.jl index e59303e2..f5fdeaa1 100644 --- a/src/pytype.jl +++ b/src/pytype.jl @@ -348,7 +348,7 @@ function pyjlwrap_repr(o::PyPtr) return pyreturn(o != C_NULL ? string("") : "") catch e - pyraise(e, catch_backtrace()) + @pyraise e return PyPtr_NULL end end @@ -395,7 +395,7 @@ function pyjlwrap_getattr(self_::PyPtr, attr__::PyPtr) end end catch e - pyraise(e, catch_backtrace()) + @pyraise e finally attr_.o = PyPtr_NULL # don't decref end From 858bc041952ae7279144472a4e068288a986b686 Mon Sep 17 00:00:00 2001 From: Galen Lynch Date: Mon, 5 Nov 2018 13:36:52 -0500 Subject: [PATCH 4/4] Allow ioraise to display Julia backtrace --- src/io.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/io.jl b/src/io.jl index 91c28625..117f3a2c 100644 --- a/src/io.jl +++ b/src/io.jl @@ -8,13 +8,13 @@ # IOBase methods: # IO objects should raise IOError for unsupported operations or failed IO -function ioraise(e) +function ioraise(e, bt = nothing) if isa(e, MethodError) || isa(e, ErrorException) ccall((@pysym :PyErr_SetString), Cvoid, (PyPtr, Cstring), (pyexc::Dict)[PyIOError], - string("Julia exception: ", e)) + showerror_string(e, bt)) else - pyraise(e) + pyraise(e, bt) end end @@ -22,7 +22,7 @@ macro with_ioraise(expr) :(try $(esc(expr)) catch e - ioraise(e) + ioraise(e, catch_backtrace()) end) end