Skip to content

Commit

Permalink
Make Julia exceptions wrapped in Python Exceptions more informative (#…
Browse files Browse the repository at this point in the history
…608)

* 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.

* 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`.

* 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.

* Allow ioraise to display Julia backtrace
  • Loading branch information
galenlynch authored and stevengj committed Nov 6, 2018
1 parent 8eb62f2 commit fe67ef2
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/callback.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function _pyjlwrap_call(f, args_::PyPtr, kw_::PyPtr)

return pyreturn(ret)
catch e
pyraise(e)
@pyraise e
finally
args.o = PyPtr_NULL # don't decref
end
Expand Down
26 changes: 21 additions & 5 deletions src/exception.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
_showerror_string(io, e, bt)
return String(take!(io))
catch
try
Expand Down Expand Up @@ -163,15 +168,26 @@ 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
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
8 changes: 4 additions & 4 deletions src/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@
# 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

macro with_ioraise(expr)
:(try
$(esc(expr))
catch e
ioraise(e)
ioraise(e, catch_backtrace())
end)
end

Expand Down
6 changes: 3 additions & 3 deletions src/pyiterator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const jlWrapIteratorType = PyTypeObject()
return pyreturn(item)
end
catch e
pyraise(e)
@pyraise e
end
return PyPtr_NULL
end
Expand All @@ -149,7 +149,7 @@ else
return pyreturn(item)
end
catch e
pyraise(e)
@pyraise e
end
return PyPtr_NULL
end
Expand All @@ -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
end
return PyPtr_NULL
end
Expand Down
4 changes: 2 additions & 2 deletions src/pytype.jl
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ function pyjlwrap_repr(o::PyPtr)
return pyreturn(o != C_NULL ? string("<PyCall.jlwrap ",unsafe_pyjlwrap_to_objref(o),">")
: "<PyCall.jlwrap NULL>")
catch e
pyraise(e)
@pyraise e
return PyPtr_NULL
end
end
Expand Down Expand Up @@ -395,7 +395,7 @@ function pyjlwrap_getattr(self_::PyPtr, attr__::PyPtr)
end
end
catch e
pyraise(e)
@pyraise e
finally
attr_.o = PyPtr_NULL # don't decref
end
Expand Down

0 comments on commit fe67ef2

Please sign in to comment.