Skip to content

Commit 62ebd0a

Browse files
authored
[RFC] Throw a special exceptions in Julia callbacks (Fix #770) (#772)
* Throw a special exceptions in julia callbacks * Address issues raised by @stevengj * move pyerror() below @pycheckn definition + address test failure * fix errors
1 parent a80e2fd commit 62ebd0a

File tree

2 files changed

+52
-15
lines changed

2 files changed

+52
-15
lines changed

src/PyCall.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ function pyimport(name::AbstractString)
494494
o = _pyimport(name)
495495
if ispynull(o)
496496
if pyerr_occurred()
497-
e = PyError("PyImport_ImportModule")
497+
e = pyerror("PyImport_ImportModule")
498498
if pyisinstance(e.val, @pyglobalobjptr(:PyExc_ImportError))
499499
# Expand message to help with common user confusions.
500500
msg = """
@@ -540,7 +540,7 @@ or alternatively you can use the Conda package directly (via
540540
`using Conda` followed by `Conda.add` etcetera).
541541
"""
542542
end
543-
e = PyError(string(e.msg, "\n\n", msg, "\n"), e)
543+
e = pyerror(string(e.msg, "\n\n", msg, "\n"), e)
544544
end
545545
throw(e)
546546
else
@@ -862,7 +862,7 @@ if pyversion >= v"3.3"
862862
else
863863
function empty!(o::PyObject)
864864
p = _getproperty(o, "clear")
865-
if p != NULL # for dict, set, etc.
865+
if p != PyNULL() # for dict, set, etc.
866866
pydecref(pycall(PyObject(o)."clear", PyObject))
867867
else
868868
for i = length(o)-1:-1:0

src/exception.jl

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
#########################################################################
2+
# A wrapper around an error that happened in a Julia callback
3+
4+
struct PyJlError <: Exception
5+
err
6+
trace
7+
end
8+
9+
function show(io::IO, e::PyJlError)
10+
println(io, "An error occured in a Julia function called from Python:")
11+
_showerror_string(io, e.err, e.trace)
12+
end
13+
14+
115
#########################################################################
216
# Wrapper around Python exceptions
317

@@ -12,15 +26,6 @@ struct PyError <: Exception
1226
# generate a PyError object. Should normally only be called when
1327
# PyErr_Occurred returns non-NULL, and clears the Python error
1428
# indicator.
15-
function PyError(msg::AbstractString)
16-
ptype, pvalue, ptraceback = Ref{PyPtr}(), Ref{PyPtr}(), Ref{PyPtr}()
17-
# equivalent of passing C pointers &exc[1], &exc[2], &exc[3]:
18-
ccall((@pysym :PyErr_Fetch), Cvoid, (Ref{PyPtr},Ref{PyPtr},Ref{PyPtr}), ptype, pvalue, ptraceback)
19-
ccall((@pysym :PyErr_NormalizeException), Cvoid, (Ref{PyPtr},Ref{PyPtr},Ref{PyPtr}), ptype, pvalue, ptraceback)
20-
new(msg, PyObject(ptype[]), PyObject(pvalue[]), PyObject(ptraceback[]))
21-
end
22-
23-
PyError(msg::AbstractString, e::PyError) = new(msg, e.T, e.val, e.traceback)
2429
end
2530

2631
function show(io::IO, e::PyError)
@@ -54,7 +59,7 @@ pyerr_occurred() = ccall((@pysym :PyErr_Occurred), PyPtr, ()) != C_NULL
5459
pyerr_clear() = ccall((@pysym :PyErr_Clear), Cvoid, ())
5560

5661
function pyerr_check(msg::AbstractString, val::Any)
57-
pyerr_occurred() && throw(PyError(msg))
62+
pyerr_occurred() && throw(pyerror(msg))
5863
val # the val argument is there just to pass through to the return value
5964
end
6065

@@ -101,6 +106,37 @@ macro pycheckz(ex)
101106
:(@pycheckv $(esc(ex)) -1)
102107
end
103108

109+
function pyerror(msg::AbstractString)
110+
ptype, pvalue, ptraceback = Ref{PyPtr}(), Ref{PyPtr}(), Ref{PyPtr}()
111+
# equivalent of passing C pointers &exc[1], &exc[2], &exc[3]:
112+
ccall((@pysym :PyErr_Fetch), Cvoid, (Ref{PyPtr},Ref{PyPtr},Ref{PyPtr}), ptype, pvalue, ptraceback)
113+
ccall((@pysym :PyErr_NormalizeException), Cvoid, (Ref{PyPtr},Ref{PyPtr},Ref{PyPtr}), ptype, pvalue, ptraceback)
114+
pyerror(msg, PyObject(ptype[]), PyObject(pvalue[]), PyObject(ptraceback[]))
115+
end
116+
117+
function pyerror(msg::AbstractString, e::PyError)
118+
pyerror(msg, e.T, e.val, e.traceback)
119+
end
120+
121+
function pyerror(msg::AbstractString, ptype::PyObject, pvalue::PyObject, ptraceback::PyObject)
122+
pargs = _getproperty(pvalue, "args")
123+
124+
# If the value of the error is a PyJlError, it was generated in a pyjlwrap callback, and
125+
# we forward it.
126+
if pargs != C_NULL
127+
args = PyObject(pargs)
128+
if length(args) > 0
129+
arg = PyObject(@pycheckn ccall((@pysym :PySequence_GetItem), PyPtr, (PyPtr,Int), args, 0))
130+
if is_pyjlwrap(arg)
131+
jarg = unsafe_pyjlwrap_to_objref(arg)
132+
jarg isa PyJlError && return jarg
133+
end
134+
end
135+
end
136+
137+
return PyError(msg, ptype, pvalue, ptraceback)
138+
end
139+
104140
#########################################################################
105141
# Mapping of Julia Exception types to Python exceptions
106142

@@ -178,8 +214,9 @@ end
178214
function pyraise(e, bt = nothing)
179215
eT = typeof(e)
180216
pyeT = haskey(pyexc::Dict, eT) ? pyexc[eT] : pyexc[Exception]
181-
ccall((@pysym :PyErr_SetString), Cvoid, (PyPtr, Cstring),
182-
pyeT, string("Julia exception: ", showerror_string(e, bt)))
217+
err = PyJlError(e, bt)
218+
ccall((@pysym :PyErr_SetObject), Cvoid, (PyPtr, PyPtr),
219+
pyeT, PyObject(err))
183220
end
184221

185222
# Second argument allows for backtraces passed to `pyraise` to be ignored.

0 commit comments

Comments
 (0)