md(s) = string(AsMarkdown(s))
function hasmethod_invariant(fn, args...; title = _title_hasmethod(fn, args), kwargs...)
return invariant(title; kwargs...) do inputs
if !(_validate_hasmethod(args)(inputs))
return "Got invalid inputs $inputs"
end
argnames = map(arg -> arg isa Symbol ? arg : arg[1], args)
argvalues = map(arg -> arg isa Symbol ? inputs[arg] : arg[2], args)
try
fn(argvalues...)
return nothing
catch e
sig = _signature(fn, argnames, argvalues)
if e isa MethodError && e.f == fn
return md("""When calling `$fn`, got a `MethodError`. This means that there
is no method implemented for the given arguments. To fix this, please
implement the following method:
""") * "\n\n " * sig
else
return (md("When calling `$fn`, got an unexpected error:") * "\n\n" *
(sprint(Base.showerror, e; context = (:color => false,)) |>
indent |> faint) *
"\n\n" *
md("""This means that there is a method matching the given arguments,
but calling it throws an error. To fix this, please debug the following
method:""") * "\n\n " * sig)
end
end
end
end
function indent(s, n = 4)
wrap(s; initial_indent = repeat(" ", n), subsequent_indent = repeat(" ", n))
end
faint(s) = "\e[2m$s\e[22m"
function _title_hasmethod(fn, args)
buf = IOBuffer()
print(buf, "Method `$(nameof(parentmodule(fn))).$(nameof(fn))(")
for (i, arg) in enumerate(args)
name = arg isa Symbol ? arg : first(arg)
print(buf, name)
i != length(args) && print(buf, ", ")
end
print(buf, ")` implemented")
return String(take!(buf))
end
function _validate_hasmethod(args)
return function (inputs)
for arg in args
inputs isa NamedTuple || return false
if arg isa Symbol
haskey(inputs, arg) || return false
end
end
return true
end
end
function _signature(fn, argnames, argvalues)
buf = IOBuffer()
bold(s) = "\e[1m$s\e[22m"
print(buf, parentmodule(fn), ".", nameof(fn), faint("("))
for (i, (name, val)) in enumerate(zip(argnames, argvalues))
print(buf, name, faint("::"), bold(nameof(typeof(val))))
i != length(argnames) && print(buf, faint(", "))
end
print(buf, faint(")"))
return String(take!(buf))
end