Skip to content

Commit

Permalink
Move custom error hints to Experimental
Browse files Browse the repository at this point in the history
Closes #35671
  • Loading branch information
timholy committed May 1, 2020
1 parent e103478 commit 6e38413
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 96 deletions.
6 changes: 4 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ New language features
Similarly, passing an `a.b` expression uses `b` as the keyword or field name ([#29333]).

* Packages can now provide custom hints to help users resolve errors by using the
`register_error_hint` function. Packages that define custom exception types
can support hints by calling `show_error_hints` from their `showerror` method. ([#35094])
experimental `Base.Experimental.register_error_hint` function.
Packages that define custom exception types can support hints by
calling the `Base.Experimental.show_error_hints` from their
`showerror` method. ([#35094])

* Support for Unicode 13.0.0 (via utf8proc 2.5) ([#35282]).

Expand Down
91 changes: 5 additions & 86 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,87 +29,6 @@ ERROR: MyException: test exception
"""
showerror(io::IO, ex) = show(io, ex)

"""
register_error_hint(handler, exceptiontype)
Register a "hinting" function `handler(io, exception)` that can
suggest potential ways for users to circumvent errors. `handler`
should examine `exception` to see whether the conditions appropriate
for a hint are met, and if so generate output to `io`.
Packages should call `register_error_hint` from within their
`__init__` function.
For specific exception types, `handler` is required to accept additional arguments:
- `MethodError`: provide `handler(io, exc::MethodError, argtypes, kwargs)`,
which splits the combined arguments into positional and keyword arguments.
When issuing a hint, the output should typically start with `\\n`.
If you define custom exception types, your `showerror` method can
support hints by calling [`show_error_hints`](@ref).
# Example
```
julia> module Hinter
only_int(x::Int) = 1
any_number(x::Number) = 2
function __init__()
register_error_hint(MethodError) do io, exc, argtypes, kwargs
if exc.f == only_int
# Color is not necessary, this is just to show it's possible.
print(io, "\\nDid you mean to call ")
printstyled(io, "`any_number`?", color=:cyan)
end
end
end
end
```
Then if you call `Hinter.only_int` on something that isn't an `Int` (thereby triggering a `MethodError`), it issues the hint:
```
julia> Hinter.only_int(1.0)
ERROR: MethodError: no method matching only_int(::Float64)
Did you mean to call `any_number`?
Closest candidates are:
...
```
!!! compat "Julia 1.5"
Custom error hints are available as of Julia 1.5.
"""
function register_error_hint(handler, exct::Type)
list = get!(()->[], _hint_handlers, exct)
push!(list, handler)
return nothing
end

const _hint_handlers = IdDict{Type,Vector{Any}}()

"""
show_error_hints(io, ex, args...)
Invoke all handlers from [`register_error_hint`](@ref) for the particular
exception type `typeof(ex)`. `args` must contain any other arguments expected by
the handler for that type.
"""
function show_error_hints(io, ex, args...)
hinters = get!(()->[], _hint_handlers, typeof(ex))
for handler in hinters
try
Base.invokelatest(handler, io, ex, args...)
catch err
tn = typeof(handler).name
@error "Hint-handler $handler for $(typeof(ex)) in $(tn.module) caused an error"
end
end
end

show_index(io::IO, x::Any) = show(io, x)
show_index(io::IO, x::Slice) = show_index(io, x.indices)
show_index(io::IO, x::LogicalIndex) = show_index(io, x.mask)
Expand Down Expand Up @@ -138,7 +57,7 @@ function showerror(io::IO, ex::BoundsError)
print(io, ']')
end
end
show_error_hints(io, ex)
Experimental.show_error_hints(io, ex)
end

function showerror(io::IO, ex::TypeError)
Expand All @@ -162,7 +81,7 @@ function showerror(io::IO, ex::TypeError)
end
print(io, ctx, ", expected ", ex.expected, ", got ", targs...)
end
show_error_hints(io, ex)
Experimental.show_error_hints(io, ex)
end

function showerror(io::IO, ex, bt; backtrace=true)
Expand Down Expand Up @@ -201,7 +120,7 @@ function showerror(io::IO, ex::DomainError)
if isdefined(ex, :msg)
print(io, ":\n", ex.msg)
end
show_error_hints(io, ex)
Experimental.show_error_hints(io, ex)
nothing
end

Expand Down Expand Up @@ -257,7 +176,7 @@ function showerror(io::IO, ex::InexactError)
print(io, "InexactError: ", ex.func, '(')
nameof(ex.T) === ex.func || print(io, ex.T, ", ")
print(io, ex.val, ')')
show_error_hints(io, ex)
Experimental.show_error_hints(io, ex)
end

typesof(args...) = Tuple{Any[ Core.Typeof(a) for a in args ]...}
Expand Down Expand Up @@ -408,7 +327,7 @@ function showerror(io::IO, ex::MethodError)
"\nYou can convert to a column vector with the vec() function.")
end
end
show_error_hints(io, ex, arg_types_param, kwargs)
Experimental.show_error_hints(io, ex, arg_types_param, kwargs)
try
show_method_candidates(io, ex, kwargs)
catch ex
Expand Down
90 changes: 90 additions & 0 deletions base/experimental.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,94 @@ macro optlevel(n::Int)
return Expr(:meta, :optlevel, n)
end

# UI features for errors

"""
Experimental.register_error_hint(handler, exceptiontype)
Register a "hinting" function `handler(io, exception)` that can
suggest potential ways for users to circumvent errors. `handler`
should examine `exception` to see whether the conditions appropriate
for a hint are met, and if so generate output to `io`.
Packages should call `register_error_hint` from within their
`__init__` function.
For specific exception types, `handler` is required to accept additional arguments:
- `MethodError`: provide `handler(io, exc::MethodError, argtypes, kwargs)`,
which splits the combined arguments into positional and keyword arguments.
When issuing a hint, the output should typically start with `\\n`.
If you define custom exception types, your `showerror` method can
support hints by calling [`Experimental.show_error_hints`](@ref).
# Example
```
julia> module Hinter
only_int(x::Int) = 1
any_number(x::Number) = 2
function __init__()
Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs
if exc.f == only_int
# Color is not necessary, this is just to show it's possible.
print(io, "\\nDid you mean to call ")
printstyled(io, "`any_number`?", color=:cyan)
end
end
end
end
```
Then if you call `Hinter.only_int` on something that isn't an `Int` (thereby triggering a `MethodError`), it issues the hint:
```
julia> Hinter.only_int(1.0)
ERROR: MethodError: no method matching only_int(::Float64)
Did you mean to call `any_number`?
Closest candidates are:
...
```
!!! compat "Julia 1.5"
Custom error hints are available as of Julia 1.5.
!!! warning
This interface is experimental and subject to change or removal without notice.
"""
function register_error_hint(handler, exct::Type)
list = get!(()->[], _hint_handlers, exct)
push!(list, handler)
return nothing
end

const _hint_handlers = IdDict{Type,Vector{Any}}()

"""
Experimental.show_error_hints(io, ex, args...)
Invoke all handlers from [`Experimental.register_error_hint`](@ref) for the particular
exception type `typeof(ex)`. `args` must contain any other arguments expected by
the handler for that type.
!!! compat "Julia 1.5"
Custom error hints are available as of Julia 1.5.
!!! warning
This interface is experimental and subject to change or removal without notice.
"""
function show_error_hints(io, ex, args...)
hinters = get!(()->[], _hint_handlers, typeof(ex))
for handler in hinters
try
Base.invokelatest(handler, io, ex, args...)
catch err
tn = typeof(handler).name
@error "Hint-handler $handler for $(typeof(ex)) in $(tn.module) caused an error"
end
end
end

end
2 changes: 0 additions & 2 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -696,10 +696,8 @@ export
backtrace,
catch_backtrace,
error,
register_error_hint,
rethrow,
retry,
show_error_hints,
systemerror,

# stack traces
Expand Down
4 changes: 2 additions & 2 deletions doc/src/base/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ Base.backtrace
Base.catch_backtrace
Base.catch_stack
Base.@assert
Base.register_error_hint
Base.show_error_hints
Base.Experimental.register_error_hint
Base.Experimental.show_error_hints
Base.ArgumentError
Base.AssertionError
Core.BoundsError
Expand Down
8 changes: 4 additions & 4 deletions test/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ function recommend_oneunit(io, ex, arg_types, kwargs)
end
end
end
@test register_error_hint(recommend_oneunit, MethodError) === nothing
@test Base.Experimental.register_error_hint(recommend_oneunit, MethodError) === nothing
let err_str
err_str = @except_str one(HasNoOne()) MethodError
@test occursin(r"MethodError: no method matching one\(::.*HasNoOne\)", err_str)
Expand All @@ -606,19 +606,19 @@ let err_str
@test occursin(r"MethodError: no method matching one\(::.*HasNoOne; value=2\)", err_str)
@test occursin("`one` doesn't take keyword arguments, that would be silly", err_str)
end
pop!(Base._hint_handlers[MethodError]) # order is undefined, don't copy this
pop!(Base.Experimental._hint_handlers[MethodError]) # order is undefined, don't copy this

function busted_hint(io, exc, notarg) # wrong number of args
print(io, "\nI don't have a hint for you, sorry")
end
@test register_error_hint(busted_hint, DomainError) === nothing
@test Base.Experimental.register_error_hint(busted_hint, DomainError) === nothing
try
sqrt(-2)
catch ex
io = IOBuffer()
@test_logs (:error, "Hint-handler busted_hint for DomainError in $(@__MODULE__) caused an error") showerror(io, ex)
end
pop!(Base._hint_handlers[DomainError]) # order is undefined, don't copy this
pop!(Base.Experimental._hint_handlers[DomainError]) # order is undefined, don't copy this


# issue #28442
Expand Down

0 comments on commit 6e38413

Please sign in to comment.