Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Throw a special exceptions in Julia callbacks (Fix #770) #772

Merged
merged 4 commits into from
May 2, 2020

Conversation

PhilipVinc
Copy link
Contributor

This PR tries to address the printing of stack traces in Julia callbacks and fix #770, while also leaving the door open for further improvements.

The problem I wanted to solve is the fact that errors and stack traces thrown by a Julia callback executed by python, such as

julia> py"$error('I am ugly')"

loses formatting when being displayed.
This is because currently we render the exception to a string when it is thrown, return it to python as a python error, which then is returned to Julia again, and displays it.

This is especially painful in wrapper packages such as Tensorflow.jl, Jax.jl or several others.

In the process formatting is lost.

My solution:
When a Julia callback throws, don't render the callback immediately. Instead return a custom Error type, PyJlError, which stores the backtrace.

When control is eventually returned to Julia, check if the python error is wrapping a Julia error. If so, show only the Julia one.

This is compatible with JuliaPy, as the rendering of PyJlError is handled by the show method.

Result:
old:

julia> using PyCall
julia> py"$error('I am ugly')"
ERROR: PyError ($(Expr(:escape, :(ccall(#= /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:38 =# @pysym(:PyEval_EvalCode), PyPtr, (PyPtr, PyPtr, PyPtr), o, globals, locals))))) <class 'RuntimeError'>
RuntimeError('Julia exception: I am ugly\nStacktrace:\n [1] error(::String) at ./error.jl:33\n [2] #invokelatest#1 at ./essentials.jl:712 [inlined]\n [3] invokelatest(::Any, ::Any) at ./essentials.jl:711\n [4] _pyjlwrap_call(::Function, ::Ptr{PyCall.PyObject_struct}, ::Ptr{PyCall.PyObject_struct}) at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/callback.jl:28\n [5] pyjlwrap_call(::Ptr{PyCall.PyObject_struct}, ::Ptr{PyCall.PyObject_struct}, ::Ptr{PyCall.PyObject_struct}) at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/callback.jl:49\n [6] macro expansion at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/exception.jl:93 [inlined]\n [7] #120 at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:38 [inlined]\n [8] disable_sigint at ./c.jl:446 [inlined]\n [9] pyeval_(::String, ::PyDict{String,PyObject,true}, ::PyDict{String,PyObject,true}, ::Int64, ::String) at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:37\n [10] top-level scope at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:230\n [11] eval(::Module, ::Any) at ./boot.jl:331\n [12] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86\n [13] run_backend(::REPL.REPLBackend) at /Users/filippovicentini/.julia/packages/Revise/Pcs5V/src/Revise.jl:1073\n [14] top-level scope at none:0\n [15] eval(::Module, ::Any) at ./boot.jl:331\n [16] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86\n [17] macro expansion at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:118 [inlined]\n [18] (::REPL.var"#26#27"{REPL.REPLBackend})() at ./task.jl:358')
  File "/Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl", line 1, in <module>
    const Py_single_input = 256  # from Python.h

Stacktrace:
 [1] pyerr_check at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/exception.jl:60 [inlined]
 [2] pyerr_check at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/exception.jl:64 [inlined]
 [3] _handle_error(::String) at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/exception.jl:81
 [4] macro expansion at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/exception.jl:95 [inlined]
 [5] #120 at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:38 [inlined]
 [6] disable_sigint at ./c.jl:446 [inlined]
 [7] pyeval_(::String, ::PyDict{String,PyObject,true}, ::PyDict{String,PyObject,true}, ::Int64, ::String) at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:37
 [8] top-level scope at /Users/filippovicentini/.julia/packages/PyCall/zqDXB/src/pyeval.jl:230
 [9] eval(::Module, ::Any) at ./boot.jl:331
 [10] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [11] run_backend(::REPL.REPLBackend) at /Users/filippovicentini/.julia/packages/Revise/Pcs5V/src/Revise.jl:1073
 [12] top-level scope at none:0

new:

julia> py"$error('I am ugly')"
ERROR: An error occured in a Julia function called from Python
I am ugly
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] #invokelatest#1 at ./essentials.jl:712 [inlined]
 [3] invokelatest(::Any, ::Any) at ./essentials.jl:711
 [4] _pyjlwrap_call(::Function, ::Ptr{PyCall.PyObject_struct}, ::Ptr{PyCall.PyObject_struct}) at /Users/filippovicentini/.julia/dev/PyCall/src/callback.jl:28
 [5] pyjlwrap_call(::Ptr{PyCall.PyObject_struct}, ::Ptr{PyCall.PyObject_struct}, ::Ptr{PyCall.PyObject_struct}) at /Users/filippovicentini/.julia/dev/PyCall/src/callback.jl:49
 [6] macro expansion at /Users/filippovicentini/.julia/dev/PyCall/src/exception.jl:117 [inlined]
 [7] #120 at /Users/filippovicentini/.julia/dev/PyCall/src/pyeval.jl:38 [inlined]
 [8] disable_sigint at ./c.jl:446 [inlined]
 [9] pyeval_(::String, ::PyDict{String,PyObject,true}, ::PyDict{String,PyObject,true}, ::Int64, ::String) at /Users/filippovicentini/.julia/dev/PyCall/src/pyeval.jl:37
 [10] top-level scope at /Users/filippovicentini/.julia/dev/PyCall/src/pyeval.jl:230
 [11] eval(::Module, ::Any) at ./boot.jl:331
 [12] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [13] run_backend(::REPL.REPLBackend) at /Users/filippovicentini/.julia/packages/Revise/Pcs5V/src/Revise.jl:1073
 [14] top-level scope at none:0
 [15] eval(::Module, ::Any) at ./boot.jl:331
 [16] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [17] macro expansion at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:118 [inlined]
 [18] (::REPL.var"#26#27"{REPL.REPLBackend})() at ./task.jl:358
Stacktrace:
 [1] pyerr_check at /Users/filippovicentini/.julia/dev/PyCall/src/exception.jl:84 [inlined]
 [2] pyerr_check at /Users/filippovicentini/.julia/dev/PyCall/src/exception.jl:88 [inlined]
 [3] _handle_error(::String) at /Users/filippovicentini/.julia/dev/PyCall/src/exception.jl:105
 [4] macro expansion at /Users/filippovicentini/.julia/dev/PyCall/src/exception.jl:119 [inlined]
 [5] #120 at /Users/filippovicentini/.julia/dev/PyCall/src/pyeval.jl:38 [inlined]
 [6] disable_sigint at ./c.jl:446 [inlined]
 [7] pyeval_(::String, ::PyDict{String,PyObject,true}, ::PyDict{String,PyObject,true}, ::Int64, ::String) at /Users/filippovicentini/.julia/dev/PyCall/src/pyeval.jl:37
 [8] top-level scope at /Users/filippovicentini/.julia/dev/PyCall/src/pyeval.jl:230
 [9] eval(::Module, ::Any) at ./boot.jl:331
 [10] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [11] run_backend(::REPL.REPLBackend) at /Users/filippovicentini/.julia/packages/Revise/Pcs5V/src/Revise.jl:1073
 [12] top-level scope at none:0

src/exception.jl Outdated Show resolved Hide resolved
src/exception.jl Outdated Show resolved Hide resolved
src/exception.jl Outdated Show resolved Hide resolved
src/exception.jl Outdated Show resolved Hide resolved
src/exception.jl Outdated Show resolved Hide resolved
src/exception.jl Outdated Show resolved Hide resolved
@PhilipVinc
Copy link
Contributor Author

So...
one error is because in this line

        if p != NULL # for dict, set, etc.

NULL is not defined.

The other one is the one with Unprintable and I'm on it...

@PhilipVinc
Copy link
Contributor Author

PhilipVinc commented May 2, 2020

Ok, the second error is... very weird.

In short, the solution is removing the parametric type from PyJlError, which is not really needed anyway.

However, I don't understand why that is the case. So below is a small investigation on the cause. Maybe it's an underlying issue in Julia, or maybe in my implementation?

I tracked it down to exactly this line

    err = PyJlError(e, bt)

The error also persists if I try to do

t=typeof(e) #executes ok
err = PyJlError{t}(e, bt)

In case it might help, this error still happens if you make Unprintable's type printable but not the value.

@stevengj
Copy link
Member

stevengj commented May 2, 2020

I think if you wanted to keep the parametric type you would need an outer constructor PyJlError(err::E, trace) where {E} = PyJlError{E}(err, trace)?

But I agree that a parameterized type is not needed here.

@stevengj
Copy link
Member

stevengj commented May 2, 2020

The error also persists if I try to do t=typeof(e); err = PyJlError{e}(e, bt)

I think you mean err = PyJlError{t}(e, bt)?

@stevengj
Copy link
Member

stevengj commented May 2, 2020

But let's leave the type as non-parametric.

@PhilipVinc
Copy link
Contributor Author

(Now tests are passing, except for an unrelated error which I'm not sure why shows up half the time).

I think you mean err = PyJlError{t}(e, bt)?

Yes, indeed.
I think I understood what the problem is: I just found out that in Julia you can throw any object as an error.

On of the tests tries throwing an Unprintable object (which is not <:Exception) and so it could not build a PyJlerror which was throwing again.. and so on.

@stevengj stevengj merged commit 62ebd0a into JuliaPy:master May 2, 2020
@stevengj
Copy link
Member

stevengj commented May 2, 2020

@tkf, hopefully this doesn't cause any problems for pyjulia?

@PhilipVinc PhilipVinc deleted the PhilipVinc/errors branch May 2, 2020 18:49
@tkf
Copy link
Member

tkf commented May 3, 2020

Thanks for the ping. I ran PyJulia test suite with PyCall master and there was no problem. I looked at the patch and played with it a bit. So far it works nicely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Julia Stack Traces thrown during python execution lose formatting
3 participants