Skip to content

Commit

Permalink
Add stacktrace colors to showing MethodErrors and method lists (#45069
Browse files Browse the repository at this point in the history
)

* Add color pass to MethodError showing, like in stacktraces

Co-authored-by: Sukera <[email protected]>
  • Loading branch information
Seelengrab and Seelengrab committed Jun 24, 2022
1 parent ef7498d commit b2b8ce8
Show file tree
Hide file tree
Showing 20 changed files with 306 additions and 164 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,7 @@ External dependencies
Tooling Improvements
---------------------

* Printing of `MethodError` and methods (such as from `methods(my_func)`) are now prettified and color consistent with printing of methods
in stacktraces. ([#45069])

<!--- generated by NEWS-update.jl: -->
7 changes: 4 additions & 3 deletions base/deprecated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ old (generic function with 1 method)
Calls to `@deprecate` without explicit type-annotations will define deprecated methods
accepting arguments of type `Any`. To restrict deprecation to a specific signature, annotate
the arguments of `old`. For example,
```jldoctest; filter = r"in Main at.*"
```jldoctest; filter = r"@ .*"
julia> new(x::Int) = x;
julia> new(x::Float64) = 2x;
julia> @deprecate old(x::Int) new(x);
julia> methods(old)
# 1 method for generic function "old":
[1] old(x::Int64) in Main at deprecated.jl:70
# 1 method for generic function "old" from Main:
[1] old(x::Int64)
@ deprecated.jl:94
```
will define and deprecate a method `old(x::Int)` that mirrors `new(x::Int)` but will not
define nor deprecate the method `old(x::Float64)`.
Expand Down
94 changes: 50 additions & 44 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ end
striptype(::Type{T}) where {T} = T
striptype(::Any) = nothing

function showerror_ambiguous(io::IO, meth, f, args)
function showerror_ambiguous(io::IO, meths, f, args)
print(io, "MethodError: ")
show_signature_function(io, isa(f, Type) ? Type{f} : typeof(f))
print(io, "(")
Expand All @@ -342,23 +342,25 @@ function showerror_ambiguous(io::IO, meth, f, args)
print(io, "::", a)
i < length(p) && print(io, ", ")
end
print(io, ") is ambiguous. Candidates:")
println(io, ") is ambiguous.\n\nCandidates:")
sigfix = Any
for m in meth
print(io, "\n ", m)
for m in meths
print(io, " ")
show(io, m; digit_align_width=-2)
println(io)
sigfix = typeintersect(m.sig, sigfix)
end
if isa(unwrap_unionall(sigfix), DataType) && sigfix <: Tuple
let sigfix=sigfix
if all(m->morespecific(sigfix, m.sig), meth)
if all(m->morespecific(sigfix, m.sig), meths)
print(io, "\nPossible fix, define\n ")
Base.show_tuple_as_call(io, :function, sigfix)
else
println(io)
print(io, "To resolve the ambiguity, try making one of the methods more specific, or ")
print(io, "adding a new method more specific than any of the existing applicable methods.")
end
end
println(io)
end
nothing
end
Expand Down Expand Up @@ -516,7 +518,7 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=()
file = string(method.file)
end
stacktrace_contract_userdir() && (file = contractuser(file))
print(iob, " at ", file, ":", line)

if !isempty(kwargs)::Bool
unexpected = Symbol[]
if isempty(kwords) || !(any(endswith(string(kword), "...") for kword in kwords))
Expand All @@ -538,6 +540,12 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=()
elseif ex.world > reinterpret(UInt, method.deleted_world)
print(iob, " (method deleted before this world age.)")
end
println(iob)

m = parentmodule_before_main(method.module)
color = get!(() -> popfirst!(STACKTRACE_MODULECOLORS), STACKTRACE_FIXEDCOLORS, m)
print_module_path_file(iob, m, string(file), line, color, 1)

# TODO: indicate if it's in the wrong world
push!(lines, (buf, right_matches))
end
Expand All @@ -546,7 +554,7 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=()

if !isempty(lines) # Display up to three closest candidates
Base.with_output_color(:normal, io) do io
print(io, "\nClosest candidates are:")
print(io, "\n\nClosest candidates are:")
sort!(lines, by = x -> -x[2])
i = 0
for line in lines
Expand All @@ -558,6 +566,7 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=()
i += 1
print(io, String(take!(line[1])))
end
println(io) # extra newline for spacing to stacktrace
end
end
end
Expand All @@ -573,20 +582,17 @@ end
# replace `sf` as needed.
const update_stackframes_callback = Ref{Function}(identity)

const STACKTRACE_MODULECOLORS = [:magenta, :cyan, :green, :yellow]
const STACKTRACE_MODULECOLORS = Iterators.Stateful(Iterators.cycle([:magenta, :cyan, :green, :yellow]))
const STACKTRACE_FIXEDCOLORS = IdDict(Base => :light_black, Core => :light_black)

function show_full_backtrace(io::IO, trace::Vector; print_linebreaks::Bool)
num_frames = length(trace)
ndigits_max = ndigits(num_frames)

modulecolordict = copy(STACKTRACE_FIXEDCOLORS)
modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS))

println(io, "\nStacktrace:")

for (i, (frame, n)) in enumerate(trace)
print_stackframe(io, i, frame, n, ndigits_max, modulecolordict, modulecolorcycler)
print_stackframe(io, i, frame, n, ndigits_max, STACKTRACE_FIXEDCOLORS, STACKTRACE_MODULECOLORS)
if i < num_frames
println(io)
print_linebreaks && println(io)
Expand Down Expand Up @@ -646,15 +652,12 @@ function show_reduced_backtrace(io::IO, t::Vector)

ndigits_max = ndigits(length(t))

modulecolordict = Dict{Module, Symbol}()
modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS))

push!(repeated_cycle, (0,0,0)) # repeated_cycle is never empty
frame_counter = 1
for i in 1:length(displayed_stackframes)
(frame, n) = displayed_stackframes[i]

print_stackframe(io, frame_counter, frame, n, ndigits_max, modulecolordict, modulecolorcycler)
print_stackframe(io, frame_counter, frame, n, ndigits_max, STACKTRACE_FIXEDCOLORS, STACKTRACE_MODULECOLORS)

if i < length(displayed_stackframes)
println(io)
Expand Down Expand Up @@ -684,22 +687,24 @@ end
# from `modulecolorcycler`.
function print_stackframe(io, i, frame::StackFrame, n::Int, digit_align_width, modulecolordict, modulecolorcycler)
m = Base.parentmodule(frame)
if m !== nothing
while parentmodule(m) !== m
pm = parentmodule(m)
pm == Main && break
m = pm
end
if !haskey(modulecolordict, m)
modulecolordict[m] = popfirst!(modulecolorcycler)
end
modulecolor = modulecolordict[m]
modulecolor = if m !== nothing
m = parentmodule_before_main(m)
get!(() -> popfirst!(modulecolorcycler), modulecolordict, m)
else
modulecolor = :default
:default
end
print_stackframe(io, i, frame, n, digit_align_width, modulecolor)
end

# Gets the topmost parent module that isn't Main
function parentmodule_before_main(m)
while parentmodule(m) !== m
pm = parentmodule(m)
pm == Main && break
m = pm
end
m
end

# Print a stack frame where the module color is set manually with `modulecolor`.
function print_stackframe(io, i, frame::StackFrame, n::Int, digit_align_width, modulecolor)
Expand Down Expand Up @@ -727,32 +732,33 @@ function print_stackframe(io, i, frame::StackFrame, n::Int, digit_align_width, m
end
println(io)

# @
printstyled(io, " " ^ (digit_align_width + 2) * "@ ", color = :light_black)
# @ Module path / file : line
print_module_path_file(io, modul, file, line, modulecolor, digit_align_width)

# inlined
printstyled(io, inlined ? " [inlined]" : "", color = :light_black)
end

function print_module_path_file(io, modul, file, line, modulecolor = :light_black, digit_align_width = 0)
printstyled(io, " " ^ (digit_align_width + 2) * "@", color = :light_black)

# module
if modul !== nothing
printstyled(io, modul, color = modulecolor)
if modul !== nothing && modulecolor !== nothing
print(io, " ")
printstyled(io, modul, color = modulecolor)
end

# filepath
pathparts = splitpath(file)
folderparts = pathparts[1:end-1]
if !isempty(folderparts)
printstyled(io, joinpath(folderparts...) * (Sys.iswindows() ? "\\" : "/"), color = :light_black)
end
stacktrace_expand_basepaths() && (file = something(find_source_file(file), file))
stacktrace_contract_userdir() && (file = contractuser(file))
print(io, " ")
dir = dirname(file)
!isempty(dir) && printstyled(io, dir, Filesystem.path_separator, color = :light_black)

# filename, separator, line
# use escape codes for formatting, printstyled can't do underlined and color
# codes are bright black (90) and underlined (4)
printstyled(io, pathparts[end], ":", line; color = :light_black, underline = true)

# inlined
printstyled(io, inlined ? " [inlined]" : "", color = :light_black)
printstyled(io, basename(file), ":", line; color = :light_black, underline = true)
end


function show_backtrace(io::IO, t::Vector)
if haskey(io, :last_shown_line_infos)
empty!(io[:last_shown_line_infos])
Expand Down
92 changes: 61 additions & 31 deletions base/methodshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ function arg_decl_parts(m::Method, html=false)
push!(tv, sig.var)
sig = sig.body
end
file = m.file
line = m.line
file, line = updated_methodloc(m)
argnames = method_argnames(m)
if length(argnames) >= m.nargs
show_env = ImmutableDict{Symbol, Any}()
Expand Down Expand Up @@ -206,34 +205,45 @@ function sym_to_string(sym)
end
end

function show(io::IO, m::Method)
function show(io::IO, m::Method; modulecolor = :light_black, digit_align_width = -1)
tv, decls, file, line = arg_decl_parts(m)
sig = unwrap_unionall(m.sig)
if sig === Tuple
# Builtin
print(io, m.name, "(...) in ", m.module)
return
end
print(io, decls[1][2], "(")
join(
io,
String[isempty(d[2]) ? d[1] : string(d[1], "::", d[2]) for d in decls[2:end]],
", ",
", ",
)
kwargs = kwarg_decl(m)
if !isempty(kwargs)
print(io, "; ")
join(io, map(sym_to_string, kwargs), ", ", ", ")
end
print(io, ")")
show_method_params(io, tv)
print(io, " in ", m.module)
if line > 0
file, line = updated_methodloc(m)
print(io, " at ", file, ":", line)
print(io, m.name, "(...)")
file = "none"
line = 0
else
print(io, decls[1][2], "(")

# arguments
for (i,d) in enumerate(decls[2:end])
printstyled(io, d[1], color=:light_black)
if !isempty(d[2])
print(io, "::")
print_type_bicolor(io, d[2], color=:bold, inner_color=:normal)
end
i < length(decls)-1 && print(io, ", ")
end

kwargs = kwarg_decl(m)
if !isempty(kwargs)
print(io, "; ")
for kw in kwargs
skw = sym_to_string(kw)
print(io, skw)
if kw != last(kwargs)
print(io, ", ")
end
end
end
print(io, ")")
show_method_params(io, tv)
end
nothing

# module & file, re-using function from errorshow.jl
println(io)
print_module_path_file(io, m.module, string(file), line, modulecolor, digit_align_width+4)
end

function show_method_list_header(io::IO, ms::MethodList, namefmt::Function)
Expand All @@ -253,16 +263,19 @@ function show_method_list_header(io::IO, ms::MethodList, namefmt::Function)
"builtin function"
: # else
"generic function")
print(io, " for ", what, " ", namedisplay)
print(io, " for ", what, " ", namedisplay, " from ")

col = get!(() -> popfirst!(STACKTRACE_MODULECOLORS), STACKTRACE_FIXEDCOLORS, parentmodule_before_main(ms.mt.module))

printstyled(io, ms.mt.module, color=col)
elseif '#' in sname
print(io, " for anonymous function ", namedisplay)
elseif mt === _TYPE_NAME.mt
print(io, " for type constructor")
else
print(io, " for callable object")
end
n > 0 && print(io, ":")
nothing
!iszero(n) && print(io, ":")
end

function show_method_table(io::IO, ms::MethodList, max::Int=-1, header::Bool=true)
Expand All @@ -279,12 +292,29 @@ function show_method_table(io::IO, ms::MethodList, max::Int=-1, header::Bool=tru
last_shown_line_infos = get(io, :last_shown_line_infos, nothing)
last_shown_line_infos === nothing || empty!(last_shown_line_infos)

modul = if mt === _TYPE_NAME.mt # type constructor
which(ms.ms[1].module, ms.ms[1].name)
else
mt.module
end

digit_align_width = length(string(max > 0 ? max : length(ms)))

for meth in ms
if max == -1 || n < max
n += 1
println(io)
print(io, "[$n] ")
show(io, meth)

print(io, " ", lpad("[$n]", digit_align_width + 2), " ")

modulecolor = if meth.module == modul
nothing
else
m = parentmodule_before_main(meth.module)
get!(() -> popfirst!(STACKTRACE_MODULECOLORS), STACKTRACE_FIXEDCOLORS, m)
end
show(io, meth; modulecolor)

file, line = updated_methodloc(meth)
if last_shown_line_infos !== nothing
push!(last_shown_line_infos, (string(file), line))
Expand Down Expand Up @@ -374,7 +404,7 @@ function show(io::IO, ::MIME"text/html", m::Method)
join(
io,
String[
isempty(d[2]) ? d[1] : string(d[1], "::<b>", d[2], "</b>") for d in decls[2:end]
isempty(d[2]) ? string(d[1]) : string(d[1], "::<b>", d[2] , "</b>") for d in decls[2:end]
],
", ",
", ",
Expand Down
Loading

0 comments on commit b2b8ce8

Please sign in to comment.