From b22f594f3c16fe3cff0e49d527a051eae971da90 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Wed, 14 Aug 2024 15:49:29 +0200 Subject: [PATCH 01/13] Fix tests on 1.11 --- Project.toml | 4 +++- test/tests.jl | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index bad33c1..4d2d064 100644 --- a/Project.toml +++ b/Project.toml @@ -6,12 +6,14 @@ version = "0.9.3" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" [compat] +REPL = "1" julia = "1" [extras] Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [targets] -test = ["Markdown", "Pkg", "Test"] +test = ["Markdown", "Pkg", "Test", "REPL"] diff --git a/test/tests.jl b/test/tests.jl index 560613c..49e2ebf 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -1,3 +1,4 @@ +using REPL # Hack to get around: https://github.com/JuliaLang/julia/issues/52986 const DSE = DocStringExtensions include("templates.jl") From 617ae4acdd7a419791ab0f87e3989db4db3baa71 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Thu, 15 Aug 2024 20:30:53 +0200 Subject: [PATCH 02/13] Add support for default values to `TYPEDSIGNATURES` This is done by making `TypedMethodSignatures` interpolatable and giving it the Expr of the thing the docstring is bound to. From the AST we can parse each arguments name, type annotation, default value, and whether or not it's variadic. Currently only the default value is used in `TYPEDSIGNATURES`, though in the future it might be an idea to use the type information as well. --- Project.toml | 6 +- src/DocStringExtensions.jl | 1 + src/abbreviations.jl | 27 +++++--- src/parsing.jl | 122 +++++++++++++++++++++++++++++++++++++ src/utilities.jl | 72 ++++++++++++++-------- test/runtests.jl | 2 + test/tests.jl | 67 ++++++++++++-------- 7 files changed, 234 insertions(+), 63 deletions(-) create mode 100644 src/parsing.jl diff --git a/Project.toml b/Project.toml index 4d2d064..9be2ee1 100644 --- a/Project.toml +++ b/Project.toml @@ -6,14 +6,16 @@ version = "0.9.3" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" [compat] +CodeTracking = "1.3.6" REPL = "1" julia = "1" [extras] +CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Markdown", "Pkg", "Test", "REPL"] +test = ["Markdown", "Pkg", "Test", "REPL", "CodeTracking"] diff --git a/src/DocStringExtensions.jl b/src/DocStringExtensions.jl index 1e1a1ce..b1f5606 100644 --- a/src/DocStringExtensions.jl +++ b/src/DocStringExtensions.jl @@ -86,6 +86,7 @@ export interpolation # Includes. +include("parsing.jl") include("utilities.jl") include("abbreviations.jl") include("templates.jl") diff --git a/src/abbreviations.jl b/src/abbreviations.jl index a40a9de..477d105 100644 --- a/src/abbreviations.jl +++ b/src/abbreviations.jl @@ -340,7 +340,11 @@ The singleton type for [`TYPEDSIGNATURES`](@ref) abbreviations. $(:FIELDS) """ -struct TypedMethodSignatures <: Abbreviation end +struct TypedMethodSignatures <: Abbreviation + expr::Union{Nothing, Expr} +end + +interpolation(::TypedMethodSignatures, expr) = TypedMethodSignatures(expr) """ An [`Abbreviation`](@ref) for including a simplified representation of all the method @@ -358,21 +362,24 @@ f(x::Int, y::Int; a, b...) ``` ```` """ -const TYPEDSIGNATURES = TypedMethodSignatures() +const TYPEDSIGNATURES = TypedMethodSignatures(nothing) + +function format(x::TypedMethodSignatures, buf, doc) + binding = doc.data[:binding] + typesig = doc.data[:typesig] + modname = doc.data[:module] + func = Docs.resolve(binding) -function format(::TypedMethodSignatures, buf, doc) - local binding = doc.data[:binding] - local typesig = doc.data[:typesig] - local modname = doc.data[:module] - local func = Docs.resolve(binding) # TODO: why is methodgroups returning invalid methods? # the methodgroups always appears to return a Vector and the size depends on whether parametric types are used # and whether default arguments are used - local groups = methodgroups(func, typesig, modname) + groups = methodgroups(func, typesig, modname) if !isempty(groups) group = groups[end] + ast_info = parse_call(x.expr) println(buf) println(buf, "```julia") + for (i, method) in enumerate(group) N = length(arguments(method)) # return a list of tuples that represent type signatures @@ -395,9 +402,11 @@ function format(::TypedMethodSignatures, buf, doc) else t = tuples[findfirst(f, tuples)] end - printmethod(buf, binding, func, method, t) + + printmethod(buf, binding, func, ast_info.args, ast_info.kwargs, t) println(buf) end + println(buf, "\n```\n") end end diff --git a/src/parsing.jl b/src/parsing.jl new file mode 100644 index 0000000..e08c7f3 --- /dev/null +++ b/src/parsing.jl @@ -0,0 +1,122 @@ +Base.@kwdef struct ASTArg + name::Union{Symbol, Nothing} = nothing + type = nothing + default = nothing + variadic::Bool = false +end + +# Parse an argument with a type annotation. +# Example input: `x::Int` +function parse_arg_with_type(arg_expr::Expr) + if arg_expr.head != :(::) + throw(ArgumentError("Argument is not a :(::) expr")) + end + + n_expr_args = length(arg_expr.args) + return if n_expr_args == 1 + # '::Int' + ASTArg(; type=arg_expr.args[1]) + elseif n_expr_args == 2 + # 'x::Int' + ASTArg(; name=arg_expr.args[1], type=arg_expr.args[2]) + end +end + +# Parse an argument with a default value. +# Example input: `x=5` +function parse_arg_with_default(arg_expr::Expr) + if arg_expr.head != :kw + throw(ArgumentError("Argument is not a :kw expr")) + end + + if arg_expr.args[1] isa Symbol + # This is an argument without a type annotation + ASTArg(; name=arg_expr.args[1], default=arg_expr.args[2]) + else + # This is an argument with a type annotation + tmp = parse_arg_with_type(arg_expr.args[1]) + ASTArg(; name=tmp.name, type=tmp.type, default=arg_expr.args[2]) + end +end + +# Parse a list of expressions, assuming the list is an argument list containing +# positional/keyword arguments. +# Example input: `(x, y::Int; z=5, kwargs...)` +function parse_arglist!(exprs, args, kwargs, is_kwarg_list=false) + list = is_kwarg_list ? kwargs : args + + for arg_expr in exprs + if arg_expr isa Symbol + # Plain argument name with no type or default value + push!(list, ASTArg(; name=arg_expr)) + elseif arg_expr.head == :(::) + # With a type annotation + push!(list, parse_arg_with_type(arg_expr)) + elseif arg_expr.head == :kw + # With a default value (and possibly a type annotation) + push!(list, parse_arg_with_default(arg_expr)) + elseif arg_expr.head == :parameters + # Keyword arguments + parse_arglist!(arg_expr.args, args, kwargs, true) + elseif arg_expr.head === :... + # Variadic argument + if arg_expr.args[1] isa Symbol + # Without a type annotation + push!(list, ASTArg(; name=arg_expr.args[1], variadic=true)) + elseif arg_expr.args[1].head === :(::) + # With a type annotation + arg_expr = arg_expr.args[1] + push!(list, ASTArg(; name=arg_expr.args[1], type=arg_expr.args[2], variadic=true)) + else + Meta.dump(arg_expr) + error("Couldn't parse variadic Expr in arg list (printed above)") + end + else + Meta.dump(arg_expr) + error("Couldn't parse Expr in arg list (printed above)") + end + end +end + +# Find a :call expression within an Expr. This will take care of ignoring other +# tokens like `where` clauses. +function find_call_expr(expr::Expr) + if expr.head === :macrocall && expr.args[1] === Symbol("@generated") + # If this is a generated function, find the first := expr to find + # the :call expr. + assignment_idx = findfirst(x -> x isa Expr && x.head === :(=), expr.args) + + expr.args[assignment_idx].args[1] + elseif expr.head === :(=) + find_call_expr(expr.args[1]) + elseif expr.head == :where + # Function with one or more `where` clauses + find_call_expr(expr.args[1]) + elseif expr.head === :function + find_call_expr(expr.args[1]) + elseif expr.head === :call + expr + else + Meta.dump(expr) + error("Can't parse current expr (printed above)") + end +end + +# Parse an expression to find a :call expr, and return as much information as +# possible about the arguments. +# Example input: `foo(x) = x^2` +function parse_call(expr::Expr) + Base.remove_linenums!(expr) + expr = find_call_expr(expr) + + if expr.head != :call + throw(ArgumentError("Argument is not a :call, cannot parse it.")) + end + + args = ASTArg[] + kwargs = ASTArg[] + # Skip the first argument because that's just the function name + parse_arglist!(expr.args[2:end], args, kwargs) + + return (; args, kwargs) +end diff --git a/src/utilities.jl b/src/utilities.jl index 0180699..84149f6 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -321,28 +321,7 @@ function find_tuples(typesig) end end -""" -$(:TYPEDSIGNATURES) - -Print a simplified representation of a method signature to `buffer`. Some of these -simplifications include: - - * no `TypeVar`s; - * no types; - * no keyword default values; - -# Examples - -```julia -f(x::Int; a = 1, b...) = x -sig = printmethod(Docs.Binding(Main, :f), f, first(methods(f))) -``` -""" -function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Method, typesig) - # TODO: print qualified? - local args = string.(arguments(method)) - local kws = string.(keywords(func, method)) - +function format_args(args::Vector{ASTArg}, typesig) # find inner tuple type function find_inner_tuple_type(t) # t is always either a UnionAll which represents a generic type or a Tuple where each parameter is the argument @@ -383,22 +362,65 @@ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Meth collect(typesig.types) args = map(args, argtypes) do arg,t + name = "" type = "" suffix = "" + default_value = "" + + if !isnothing(arg.name) + name = arg.name + end if isvarargtype(t) t = vararg_eltype(t) suffix = "..." + elseif arg.variadic + # This extra branch is here for kwargs, where we don't have type + # information. + suffix = "..." end - if t!==Any + if t !== Any type = "::$t" end + if !isnothing(arg.default) + default_value = "=$(arg.default)" + end - "$arg$type$suffix" + "$name$type$suffix$default_value" end + return args +end + +""" +$(:TYPEDSIGNATURES) + +Print a simplified representation of a method signature to `buffer`. Some of these +simplifications include: + + * no `TypeVar`s; + * no types; + * no keyword default values; + +# Examples + +```julia +f(x::Int; a = 1, b...) = x +sig = printmethod(Docs.Binding(Main, :f), f, first(methods(f))) +``` +""" +function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, + args::Vector{ASTArg}, kws::Vector{ASTArg}, typesig) + formatted_args = format_args(args, typesig) + # We don't have proper type information for keyword arguments like we do + # with `typesig` for positional arguments, so we assume they're all Any. An + # alternative would be to use the types extracted from the AST, but that + # might not exactly match the types of positional arguments (e.g. an alias + # type would be printed as the underlying type for positional arguments but + # under the alias for keyword arguments). + formatted_kws = format_args(kws, NTuple{length(kws), Any}) rt = Base.return_types(func, typesig) - return printmethod_format(buffer, string(binding.var), args, string.(kws); + return printmethod_format(buffer, string(binding.var), formatted_args, formatted_kws; return_type = length(rt) >= 1 && rt[1] !== Nothing && rt[1] !== Union{} ? " -> $(rt[1])" : "") diff --git a/test/runtests.jl b/test/runtests.jl index 79ec958..292a278 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,6 @@ using DocStringExtensions +import DocStringExtensions: TypedMethodSignatures + using Test import Markdown import LibGit2 diff --git a/test/tests.jl b/test/tests.jl index 49e2ebf..beb1bab 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -1,10 +1,21 @@ using REPL # Hack to get around: https://github.com/JuliaLang/julia/issues/52986 +import CodeTracking: code_string const DSE = DocStringExtensions include("templates.jl") include("interpolation.jl") include("TestModule/M.jl") +# Helper function to get the Expr of a function. In some cases the argument +# types will need to be explicitly given. +function get_expr(f::Function, arg_types...) + if isempty(arg_types) + arg_types = Base.default_tt(f) + end + + Meta.parse(code_string(f, arg_types)) +end + # initialize a test repo in test/TestModule which is needed for some tests function with_test_repo(f) repo = LibGit2.init(joinpath(@__DIR__, "TestModule")) @@ -214,7 +225,8 @@ end :typesig => Tuple{M.A}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + + DSE.format(TypedMethodSignatures(get_expr(M.h_1)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) f = str -> replace(str, " " => "") @@ -231,24 +243,25 @@ end :typesig => Tuple{String}, :module => M, ) - DSE.format(TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.g_2)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\ng_2(x::String)", str) @test occursin("\n```\n", str) + h_expr = get_expr(M.h, Int, Int, Int) doc.data = Dict( :binding => Docs.Binding(M, :h), :typesig => Tuple{Int, Int, Int}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(h_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if typeof(1) === Int64 - @test occursin("\nh(x::Int64, y::Int64, z::Int64; kwargs...) -> Int64\n", str) + @test occursin("\nh(x::Int64, y::Int64=2, z::Int64=3; kwargs...) -> Int64\n", str) else - @test occursin("\nh(x::Int32, y::Int32, z::Int32; kwargs...) -> Int32\n", str) + @test occursin("\nh(x::Int32, y::Int32=2, z::Int32=3; kwargs...) -> Int32\n", str) end @test occursin("\n```\n", str) @@ -257,14 +270,14 @@ end :typesig => Tuple{Int}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(h_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if typeof(1) === Int64 # On 1.10+, automatically generated methods have keywords in the metadata, # hence the display difference between Julia versions. if VERSION >= v"1.10" - @test occursin("\nh(x::Int64; ...) -> Int64\n", str) + @test occursin("\nh(x::Int64; kwargs...) -> Int64\n", str) else @test occursin("\nh(x::Int64) -> Int64\n", str) end @@ -284,7 +297,7 @@ end :typesig => Tuple{T} where T, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_0)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_0(x) -> Any\n", str) @@ -295,12 +308,13 @@ end :typesig => Union{Tuple{String}, Tuple{String, T}, Tuple{String, T, T}, Tuple{T}} where T <: Number, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + k_1_expr = get_expr(M.k_1, String, Int, Int) + DSE.format(TypedMethodSignatures(k_1_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_1(x::String) -> String\n", str) - @test occursin("\nk_1(x::String, y::Number) -> String\n", str) - @test occursin("\nk_1(x::String, y::Number, z::Number) -> String\n", str) + @test occursin("\nk_1(x::String, y::Number=0) -> String\n", str) + @test occursin("\nk_1(x::String, y::Number=0, z::Number=zero(T)) -> String\n", str) @test occursin("\n```\n", str) doc.data = Dict( @@ -308,8 +322,7 @@ end :typesig => (Union{Tuple{String, U, T}, Tuple{T}, Tuple{U}} where T <: Number) where U <: Complex, :module => M, ) - - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_2)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("k_2(x::String, y::Complex, z::Number) -> String", str) @@ -320,7 +333,7 @@ end :typesig => (Union{Tuple{Any, T, U}, Tuple{U}, Tuple{T}} where U <: Any) where T <: Any, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_3)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_3(x, y, z) -> Any\n", str) @@ -331,15 +344,15 @@ end :typesig => Union{Tuple{String}, Tuple{String, Int}}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_4, String, Int)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if VERSION > v"1.3.0" @test occursin("\nk_4(::String)\n", str) if typeof(1) === Int64 - @test occursin("\nk_4(::String, ::Int64)\n", str) + @test occursin("\nk_4(::String, ::Int64=0)\n", str) else - @test occursin("\nk_4(::String, ::Int32)\n", str) + @test occursin("\nk_4(::String, ::Int32=0)\n", str) end else # TODO: remove this test when julia 1.0.0 support is dropped. @@ -354,12 +367,12 @@ end :typesig => Union{Tuple{Type{T}, String}, Tuple{Type{T}, String, Union{Nothing, Function}}, Tuple{T}} where T <: Number, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_5, Type{Int}, String, Nothing)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if VERSION > v"1.3.0" @test occursin("\nk_5(::Type{T<:Number}, x::String) -> String\n", str) - @test occursin("\nk_5(\n ::Type{T<:Number},\n x::String,\n func::Union{Nothing, Function}\n) -> String\n", str) + @test occursin("\nk_5(\n ::Type{T<:Number},\n x::String,\n func::Union{Nothing, Function}=nothing\n) -> String\n", str) @test occursin("\n```\n", str) else # TODO: remove this test when julia 1.0.0 support is dropped. @@ -373,7 +386,7 @@ end :typesig => Union{Tuple{Vector{T}}, Tuple{T}} where T <: Number, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_6)), buf, doc) f = str -> replace(str, " " => "") str = String(take!(buf)) str = f(str) @@ -391,15 +404,15 @@ end :typesig => Union{Tuple{Union{Nothing, T}}, Tuple{T}, Tuple{Union{Nothing, T}, T}} where T<:Integer, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_7, Nothing, Int)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if VERSION >= v"1.6" && VERSION < v"1.7" @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer\n) -> Union{Nothing, Integer}\n", str) - @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer\n) -> Union{Nothing, Integer}\n", str) + @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer=zero(T)\n) -> Union{Nothing, Integer}\n", str) else @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer\n) -> Union{Nothing, T} where T<:Integer\n", str) - @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer\n) -> Union{Nothing, T} where T<:Integer\n", str) + @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer=zero(T)\n) -> Union{Nothing, T} where T<:Integer\n", str) end @test occursin("\n```\n", str) @@ -408,7 +421,7 @@ end :typesig => Union{Tuple{Any}}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_8)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_8(x) -> Any\n", str) @@ -419,7 +432,7 @@ end :typesig => Union{Tuple{T where T}}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_9)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_9(x) -> Any\n", str) @@ -432,7 +445,7 @@ end :typesig => Union{Tuple{Int, Vararg{Any}}}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_11)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_11(x::Int64, xs...) -> Int64\n", str) @@ -443,7 +456,7 @@ end :typesig => Union{Tuple{Int, Vararg{Real}}}, :module => M, ) - DSE.format(DSE.TYPEDSIGNATURES, buf, doc) + DSE.format(TypedMethodSignatures(get_expr(M.k_12)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_12(x::Int64, xs::Real...) -> Int64\n", str) From 1c80865682559a4dfe928b94f9a6591821331057 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Fri, 16 Aug 2024 11:54:38 +0200 Subject: [PATCH 03/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- src/parsing.jl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/parsing.jl b/src/parsing.jl index e08c7f3..f03a3ee 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -8,7 +8,7 @@ end # Parse an argument with a type annotation. # Example input: `x::Int` function parse_arg_with_type(arg_expr::Expr) - if arg_expr.head != :(::) + if !Meta.isexpr(arg_expr, :(::)) throw(ArgumentError("Argument is not a :(::) expr")) end @@ -25,7 +25,7 @@ end # Parse an argument with a default value. # Example input: `x=5` function parse_arg_with_default(arg_expr::Expr) - if arg_expr.head != :kw + if !Meta.isexpr(arg_expr, :kw) throw(ArgumentError("Argument is not a :kw expr")) end @@ -49,21 +49,21 @@ function parse_arglist!(exprs, args, kwargs, is_kwarg_list=false) if arg_expr isa Symbol # Plain argument name with no type or default value push!(list, ASTArg(; name=arg_expr)) - elseif arg_expr.head == :(::) + elseif Meta.isexpr(arg_expr, :(::)) # With a type annotation push!(list, parse_arg_with_type(arg_expr)) - elseif arg_expr.head == :kw + elseif Meta.isexpr(arg_expr, :kw) # With a default value (and possibly a type annotation) push!(list, parse_arg_with_default(arg_expr)) - elseif arg_expr.head == :parameters + elseif Meta.isexpr(arg_expr, :parameters) # Keyword arguments parse_arglist!(arg_expr.args, args, kwargs, true) - elseif arg_expr.head === :... + elseif Meta.isexpr(arg_expr, :...) # Variadic argument if arg_expr.args[1] isa Symbol # Without a type annotation push!(list, ASTArg(; name=arg_expr.args[1], variadic=true)) - elseif arg_expr.args[1].head === :(::) + elseif Meta.isexpr(arg_expr.args[1], :(::)) # With a type annotation arg_expr = arg_expr.args[1] push!(list, ASTArg(; name=arg_expr.args[1], type=arg_expr.args[2], variadic=true)) @@ -81,20 +81,20 @@ end # Find a :call expression within an Expr. This will take care of ignoring other # tokens like `where` clauses. function find_call_expr(expr::Expr) - if expr.head === :macrocall && expr.args[1] === Symbol("@generated") + if Meta.isexpr(expr, :macrocall) && expr.args[1] === Symbol("@generated") # If this is a generated function, find the first := expr to find # the :call expr. - assignment_idx = findfirst(x -> x isa Expr && x.head === :(=), expr.args) + assignment_idx = findfirst(x -> x isa Expr && Meta.isexpr(x, :(=)), expr.args) expr.args[assignment_idx].args[1] - elseif expr.head === :(=) + elseif Meta.isexpr(expr, :(=)) find_call_expr(expr.args[1]) - elseif expr.head == :where + elseif Meta.isexpr(expr, :where) # Function with one or more `where` clauses find_call_expr(expr.args[1]) - elseif expr.head === :function + elseif Meta.isexpr(expr, :function) find_call_expr(expr.args[1]) - elseif expr.head === :call + elseif Meta.isexpr(expr, :call) expr else Meta.dump(expr) @@ -109,7 +109,7 @@ function parse_call(expr::Expr) Base.remove_linenums!(expr) expr = find_call_expr(expr) - if expr.head != :call + if Meta.isexpr(expr, :call) throw(ArgumentError("Argument is not a :call, cannot parse it.")) end From 594593eebc90203dc7443d0cbeff69eede7feaaf Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Fri, 16 Aug 2024 11:58:45 +0200 Subject: [PATCH 04/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- src/parsing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parsing.jl b/src/parsing.jl index f03a3ee..aa1b1dd 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -109,7 +109,7 @@ function parse_call(expr::Expr) Base.remove_linenums!(expr) expr = find_call_expr(expr) - if Meta.isexpr(expr, :call) + if !Meta.isexpr(expr, :call) throw(ArgumentError("Argument is not a :call, cannot parse it.")) end From de0f36b50f35e798a790e704119bc1c2aaa6b51a Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Sun, 18 Aug 2024 16:44:44 +0200 Subject: [PATCH 05/13] Combine TypedMethodSignatures and MethodSignatures --- src/abbreviations.jl | 54 ++++++++---------------------------- src/utilities.jl | 19 +++++++------ test/runtests.jl | 1 - test/tests.jl | 65 ++++++++++++++++++++++++-------------------- 4 files changed, 59 insertions(+), 80 deletions(-) diff --git a/src/abbreviations.jl b/src/abbreviations.jl index 477d105..c75fe5d 100644 --- a/src/abbreviations.jl +++ b/src/abbreviations.jl @@ -290,7 +290,12 @@ The singleton type for [`SIGNATURES`](@ref) abbreviations. $(:FIELDS) """ -struct MethodSignatures <: Abbreviation end +struct MethodSignatures <: Abbreviation + expr::Union{Nothing, Expr} + print_types::Bool +end + +interpolation(ms::MethodSignatures, expr) = MethodSignatures(expr, ms.print_types) """ An [`Abbreviation`](@ref) for including a simplified representation of all the method @@ -308,43 +313,7 @@ f(x, y; a, b...) ``` ```` """ -const SIGNATURES = MethodSignatures() - -function format(::MethodSignatures, buf, doc) - local binding = doc.data[:binding] - local typesig = doc.data[:typesig] - local modname = doc.data[:module] - local func = Docs.resolve(binding) - local groups = methodgroups(func, typesig, modname) - - if !isempty(groups) - println(buf) - println(buf, "```julia") - for group in groups - for method in group - printmethod(buf, binding, func, method) - println(buf) - end - end - println(buf, "\n```\n") - end -end - - -# -# `TypedMethodSignatures` -# - -""" -The singleton type for [`TYPEDSIGNATURES`](@ref) abbreviations. - -$(:FIELDS) -""" -struct TypedMethodSignatures <: Abbreviation - expr::Union{Nothing, Expr} -end - -interpolation(::TypedMethodSignatures, expr) = TypedMethodSignatures(expr) +const SIGNATURES = MethodSignatures(nothing, false) """ An [`Abbreviation`](@ref) for including a simplified representation of all the method @@ -362,9 +331,9 @@ f(x::Int, y::Int; a, b...) ``` ```` """ -const TYPEDSIGNATURES = TypedMethodSignatures(nothing) +const TYPEDSIGNATURES = MethodSignatures(nothing, true) -function format(x::TypedMethodSignatures, buf, doc) +function format(ms::MethodSignatures, buf, doc) binding = doc.data[:binding] typesig = doc.data[:typesig] modname = doc.data[:module] @@ -376,7 +345,8 @@ function format(x::TypedMethodSignatures, buf, doc) groups = methodgroups(func, typesig, modname) if !isempty(groups) group = groups[end] - ast_info = parse_call(x.expr) + ast_info = parse_call(ms.expr) + println(buf) println(buf, "```julia") @@ -403,7 +373,7 @@ function format(x::TypedMethodSignatures, buf, doc) t = tuples[findfirst(f, tuples)] end - printmethod(buf, binding, func, ast_info.args, ast_info.kwargs, t) + printmethod(buf, binding, func, ast_info.args, ast_info.kwargs, t, ms.print_types) println(buf) end diff --git a/src/utilities.jl b/src/utilities.jl index 84149f6..8798c85 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -321,7 +321,7 @@ function find_tuples(typesig) end end -function format_args(args::Vector{ASTArg}, typesig) +function format_args(args::Vector{ASTArg}, typesig, print_types) # find inner tuple type function find_inner_tuple_type(t) # t is always either a UnionAll which represents a generic type or a Tuple where each parameter is the argument @@ -369,6 +369,8 @@ function format_args(args::Vector{ASTArg}, typesig) if !isnothing(arg.name) name = arg.name + elseif isnothing(arg.name) && (t === Any || !print_types) + name = "_" end if isvarargtype(t) t = vararg_eltype(t) @@ -378,7 +380,7 @@ function format_args(args::Vector{ASTArg}, typesig) # information. suffix = "..." end - if t !== Any + if print_types && t !== Any type = "::$t" end if !isnothing(arg.default) @@ -409,21 +411,22 @@ sig = printmethod(Docs.Binding(Main, :f), f, first(methods(f))) ``` """ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, - args::Vector{ASTArg}, kws::Vector{ASTArg}, typesig) - formatted_args = format_args(args, typesig) + args::Vector{ASTArg}, kws::Vector{ASTArg}, + typesig, print_types::Bool) + formatted_args = format_args(args, typesig, print_types) + # We don't have proper type information for keyword arguments like we do # with `typesig` for positional arguments, so we assume they're all Any. An # alternative would be to use the types extracted from the AST, but that # might not exactly match the types of positional arguments (e.g. an alias # type would be printed as the underlying type for positional arguments but # under the alias for keyword arguments). - formatted_kws = format_args(kws, NTuple{length(kws), Any}) + formatted_kws = format_args(kws, NTuple{length(kws), Any}, print_types) rt = Base.return_types(func, typesig) + can_print_rt = print_types && length(rt) >= 1 && rt[1] !== Nothing && rt[1] !== Union{} return printmethod_format(buffer, string(binding.var), formatted_args, formatted_kws; - return_type = - length(rt) >= 1 && rt[1] !== Nothing && rt[1] !== Union{} ? - " -> $(rt[1])" : "") + return_type = can_print_rt ? " -> $(rt[1])" : "") end printmethod(b, f, m) = String(take!(printmethod(IOBuffer(), b, f, m))) diff --git a/test/runtests.jl b/test/runtests.jl index 292a278..5b31f2f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,4 @@ using DocStringExtensions -import DocStringExtensions: TypedMethodSignatures using Test import Markdown diff --git a/test/tests.jl b/test/tests.jl index beb1bab..61f24c4 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -1,5 +1,6 @@ using REPL # Hack to get around: https://github.com/JuliaLang/julia/issues/52986 import CodeTracking: code_string +import DocStringExtensions: MethodSignatures const DSE = DocStringExtensions include("templates.jl") @@ -144,30 +145,34 @@ end end @testset "method signatures" begin + UntypedSignatures(x) = MethodSignatures(x, false) + doc.data = Dict( :binding => Docs.Binding(M, :f), :typesig => Tuple{Any}, :module => M, ) - DSE.format(SIGNATURES, buf, doc) + + DSE.format(UntypedSignatures(get_expr(M.f)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nf(x)\n", str) @test occursin("\n```\n", str) + g_expr = get_expr(M.g, Int, Int, Int) doc.data = Dict( :binding => Docs.Binding(M, :g), :typesig => Union{Tuple{}, Tuple{Any}}, :module => M, ) - DSE.format(SIGNATURES, buf, doc) + DSE.format(UntypedSignatures(g_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) # On 1.10+, automatically generated methods have keywords in the metadata, # hence the display difference between Julia versions. if VERSION >= v"1.10" - @test occursin("\ng(; ...)\n", str) - @test occursin("\ng(x; ...)\n", str) + @test occursin("\ng(; kwargs...)\n", str) + @test occursin("\ng(x=1; kwargs...)\n", str) else @test occursin("\ng()\n", str) @test occursin("\ng()\n", str) @@ -179,21 +184,21 @@ end :typesig => Union{Tuple{}, Tuple{Any}, Tuple{Any, Any}, Tuple{Any, Any, Any}}, :module => M, ) - DSE.format(SIGNATURES, buf, doc) + DSE.format(UntypedSignatures(g_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) # On 1.10+, automatically generated methods have keywords in the metadata, # hence the display difference between Julia versions. if VERSION >= v"1.10" - @test occursin("\ng(; ...)\n", str) - @test occursin("\ng(x; ...)\n", str) - @test occursin("\ng(x, y; ...)\n", str) + @test occursin("\ng(; kwargs...)\n", str) + @test occursin("\ng(x=1; kwargs...)\n", str) + @test occursin("\ng(x=1, y=2; kwargs...)\n", str) else @test occursin("\ng()\n", str) - @test occursin("\ng(x)\n", str) - @test occursin("\ng(x, y)\n", str) + @test occursin("\ng(x=1)\n", str) + @test occursin("\ng(x=1, y=2)\n", str) end - @test occursin("\ng(x, y, z; kwargs...)\n", str) + @test occursin("\ng(x=1, y=2, z=3; kwargs...)\n", str) @test occursin("\n```\n", str) doc.data = Dict( @@ -201,7 +206,7 @@ end :typesig => Tuple{Any}, :module => M, ) - DSE.format(SIGNATURES, buf, doc) + DSE.format(UntypedSignatures(get_expr(M.g_1)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\ng_1(x)\n", str) @@ -212,7 +217,7 @@ end :typesig => Union{Tuple{Any, Int, Any}}, :module => M, ) - DSE.format(SIGNATURES, buf, doc) + DSE.format(UntypedSignatures(get_expr(M.h_4)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nh_4(x, _, z)\n", str) @@ -226,7 +231,9 @@ end :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.h_1)), buf, doc) + TypedSignatures(x) = MethodSignatures(x, true) + + DSE.format(TypedSignatures(get_expr(M.h_1)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) f = str -> replace(str, " " => "") @@ -243,7 +250,7 @@ end :typesig => Tuple{String}, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.g_2)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.g_2)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\ng_2(x::String)", str) @@ -255,7 +262,7 @@ end :typesig => Tuple{Int, Int, Int}, :module => M, ) - DSE.format(TypedMethodSignatures(h_expr), buf, doc) + DSE.format(TypedSignatures(h_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if typeof(1) === Int64 @@ -270,7 +277,7 @@ end :typesig => Tuple{Int}, :module => M, ) - DSE.format(TypedMethodSignatures(h_expr), buf, doc) + DSE.format(TypedSignatures(h_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if typeof(1) === Int64 @@ -297,7 +304,7 @@ end :typesig => Tuple{T} where T, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_0)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_0)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_0(x) -> Any\n", str) @@ -309,7 +316,7 @@ end :module => M, ) k_1_expr = get_expr(M.k_1, String, Int, Int) - DSE.format(TypedMethodSignatures(k_1_expr), buf, doc) + DSE.format(TypedSignatures(k_1_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_1(x::String) -> String\n", str) @@ -322,7 +329,7 @@ end :typesig => (Union{Tuple{String, U, T}, Tuple{T}, Tuple{U}} where T <: Number) where U <: Complex, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_2)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_2)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("k_2(x::String, y::Complex, z::Number) -> String", str) @@ -333,7 +340,7 @@ end :typesig => (Union{Tuple{Any, T, U}, Tuple{U}, Tuple{T}} where U <: Any) where T <: Any, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_3)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_3)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_3(x, y, z) -> Any\n", str) @@ -344,7 +351,7 @@ end :typesig => Union{Tuple{String}, Tuple{String, Int}}, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_4, String, Int)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_4, String, Int)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if VERSION > v"1.3.0" @@ -367,7 +374,7 @@ end :typesig => Union{Tuple{Type{T}, String}, Tuple{Type{T}, String, Union{Nothing, Function}}, Tuple{T}} where T <: Number, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_5, Type{Int}, String, Nothing)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_5, Type{Int}, String, Nothing)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if VERSION > v"1.3.0" @@ -386,7 +393,7 @@ end :typesig => Union{Tuple{Vector{T}}, Tuple{T}} where T <: Number, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_6)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_6)), buf, doc) f = str -> replace(str, " " => "") str = String(take!(buf)) str = f(str) @@ -404,7 +411,7 @@ end :typesig => Union{Tuple{Union{Nothing, T}}, Tuple{T}, Tuple{Union{Nothing, T}, T}} where T<:Integer, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_7, Nothing, Int)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_7, Nothing, Int)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) if VERSION >= v"1.6" && VERSION < v"1.7" @@ -421,7 +428,7 @@ end :typesig => Union{Tuple{Any}}, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_8)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_8)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_8(x) -> Any\n", str) @@ -432,7 +439,7 @@ end :typesig => Union{Tuple{T where T}}, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_9)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_9)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_9(x) -> Any\n", str) @@ -445,7 +452,7 @@ end :typesig => Union{Tuple{Int, Vararg{Any}}}, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_11)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_11)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_11(x::Int64, xs...) -> Int64\n", str) @@ -456,7 +463,7 @@ end :typesig => Union{Tuple{Int, Vararg{Real}}}, :module => M, ) - DSE.format(TypedMethodSignatures(get_expr(M.k_12)), buf, doc) + DSE.format(TypedSignatures(get_expr(M.k_12)), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) @test occursin("\nk_12(x::Int64, xs::Real...) -> Int64\n", str) From d1b97f72defbd58c737544c2cb1c9bf8e04faa98 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Sun, 18 Aug 2024 16:45:16 +0200 Subject: [PATCH 06/13] Delete obsolete template_hook() method --- src/templates.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/templates.jl b/src/templates.jl index 83fb988..d66f217 100644 --- a/src/templates.jl +++ b/src/templates.jl @@ -119,11 +119,8 @@ function template_hook(source::LineNumberNode, mod::Module, docstr, expr::Expr) return (source, mod, docstr, expr) end -function template_hook(docstr, expr::Expr) - source, mod, docstr, expr::Expr = template_hook(LineNumberNode(0), current_module(), docstr, expr) - docstr, expr -end - +# This definition looks a bit weird, but in combination with hook!() the effect +# is that template_hook() will fall back to calling the default expander(). template_hook(args...) = args get_template(t::Dict, k::Symbol) = haskey(t, k) ? t[k] : get(t, :DEFAULT, Any[DOCSTRING]) From fe01fe305f0b6f744c53a19035661e58b4bc1506 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Mon, 19 Aug 2024 16:22:43 +0200 Subject: [PATCH 07/13] Make the Expr optional for MethodSignatures It won't be available when used in templates. --- src/abbreviations.jl | 4 ++-- src/templates.jl | 6 ++++++ src/utilities.jl | 33 +++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/abbreviations.jl b/src/abbreviations.jl index c75fe5d..a7ab924 100644 --- a/src/abbreviations.jl +++ b/src/abbreviations.jl @@ -345,7 +345,7 @@ function format(ms::MethodSignatures, buf, doc) groups = methodgroups(func, typesig, modname) if !isempty(groups) group = groups[end] - ast_info = parse_call(ms.expr) + ast_info = isnothing(ms.expr) ? nothing : parse_call(ms.expr) println(buf) println(buf, "```julia") @@ -373,7 +373,7 @@ function format(ms::MethodSignatures, buf, doc) t = tuples[findfirst(f, tuples)] end - printmethod(buf, binding, func, ast_info.args, ast_info.kwargs, t, ms.print_types) + printmethod(buf, binding, func, method, ast_info, t, ms.print_types) println(buf) end diff --git a/src/templates.jl b/src/templates.jl index d66f217..8390727 100644 --- a/src/templates.jl +++ b/src/templates.jl @@ -38,6 +38,12 @@ replacement docstring generated from the template. \""" ``` +Note that a significant limitation of docstring templates is that the +abbreviations used will be declared separately from the bindings that they +operate on, which means that they will not have access to the bindings +`Expr`'s. That will disable `TYPEDSIGNATURES` and `SIGNATURES` from showing +default [keyword ]argument values in docstrings. + `DEFAULT` is the default template that is applied to a docstring if no other template definitions match the documented expression. The `DOCSTRING` abbreviation is used to mark the location in the template where the actual docstring body will be spliced into each diff --git a/src/utilities.jl b/src/utilities.jl index 8798c85..fb37433 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -410,18 +410,27 @@ f(x::Int; a = 1, b...) = x sig = printmethod(Docs.Binding(Main, :f), f, first(methods(f))) ``` """ -function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, - args::Vector{ASTArg}, kws::Vector{ASTArg}, - typesig, print_types::Bool) - formatted_args = format_args(args, typesig, print_types) - - # We don't have proper type information for keyword arguments like we do - # with `typesig` for positional arguments, so we assume they're all Any. An - # alternative would be to use the types extracted from the AST, but that - # might not exactly match the types of positional arguments (e.g. an alias - # type would be printed as the underlying type for positional arguments but - # under the alias for keyword arguments). - formatted_kws = format_args(kws, NTuple{length(kws), Any}, print_types) +function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Method, + ast_info, typesig, print_types::Bool) + local formatted_args + local formatted_kws + + if isnothing(ast_info) + formatted_args = string.(arguments(method)) + formatted_kws = string.(keywords(func, method)) + else + formatted_args = format_args(ast_info.args, typesig, print_types) + + # We don't have proper type information for keyword arguments like we do + # with `typesig` for positional arguments, so we assume they're all Any. An + # alternative would be to use the types extracted from the AST, but that + # might not exactly match the types of positional arguments (e.g. an alias + # type would be printed as the underlying type for positional arguments but + # under the alias for keyword arguments). + kws = ast_info.kwargs + formatted_kws = format_args(kws, NTuple{length(kws), Any}, print_types) + end + rt = Base.return_types(func, typesig) can_print_rt = print_types && length(rt) >= 1 && rt[1] !== Nothing && rt[1] !== Union{} From 1532071e43c9da21c4ef66f1d7b4949a0d9ecc54 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Mon, 19 Aug 2024 16:56:49 +0200 Subject: [PATCH 08/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- src/parsing.jl | 39 ++++++++++++++++++++------------------- test/TestModule/M.jl | 5 +++++ test/tests.jl | 10 +++++++++- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/parsing.jl b/src/parsing.jl index aa1b1dd..9308c96 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -79,27 +79,28 @@ function parse_arglist!(exprs, args, kwargs, is_kwarg_list=false) end # Find a :call expression within an Expr. This will take care of ignoring other -# tokens like `where` clauses. -function find_call_expr(expr::Expr) - if Meta.isexpr(expr, :macrocall) && expr.args[1] === Symbol("@generated") - # If this is a generated function, find the first := expr to find - # the :call expr. - assignment_idx = findfirst(x -> x isa Expr && Meta.isexpr(x, :(=)), expr.args) +# tokens like `where` clauses. It will return `nothing` if a :call expression +# wasn't found. +function find_call_expr(obj) + if Meta.isexpr(obj, :call) + # Base case: we've found the :call expression + return obj + elseif obj isa Symbol || (obj isa Expr && isempty(obj.args)) + # Base case: this is the end of a branch in the expression tree + return nothing + end - expr.args[assignment_idx].args[1] - elseif Meta.isexpr(expr, :(=)) - find_call_expr(expr.args[1]) - elseif Meta.isexpr(expr, :where) - # Function with one or more `where` clauses - find_call_expr(expr.args[1]) - elseif Meta.isexpr(expr, :function) - find_call_expr(expr.args[1]) - elseif Meta.isexpr(expr, :call) - expr - else - Meta.dump(expr) - error("Can't parse current expr (printed above)") + # Recursive case: recurse over all the Expr arguments + for arg in obj.args + if arg isa Expr + result = find_call_expr(arg) + if !isnothing(result) + return result + end + end end + + return nothing end # Parse an expression to find a :call expr, and return as much information as diff --git a/test/TestModule/M.jl b/test/TestModule/M.jl index 01093bd..ce94d10 100644 --- a/test/TestModule/M.jl +++ b/test/TestModule/M.jl @@ -49,6 +49,11 @@ struct K K(; a = 1) = new() end +macro m1(expr) expr end +macro m2(expr) expr end + +@m1 @m2 l(x::Int, y=1) = x + y + abstract type AbstractType1 <: Integer end abstract type AbstractType2{S, T <: Integer} <: Integer end diff --git a/test/tests.jl b/test/tests.jl index 61f24c4..64de8d6 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -471,7 +471,15 @@ end end - + doc.data = Dict( + :binding => Docs.Binding(M, :l), + :typesig => Union{Tuple{Int}, Tuple{Int, Any}}, + :module => M, + ) + DSE.format(TypedSignatures(get_expr(M.l, Int, Int)), buf, doc) + str = String(take!(buf)) + @test occursin("\nl(x::Int64) -> Int64\n", str) + @test occursin("\nl(x::Int64, y=1) -> Any\n", str) end @testset "function names" begin From 7fed390997fc159ec12a6ac2331eea207afb5d42 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Tue, 20 Aug 2024 10:02:13 +0200 Subject: [PATCH 09/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- src/parsing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parsing.jl b/src/parsing.jl index 9308c96..1e1c789 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -111,7 +111,7 @@ function parse_call(expr::Expr) expr = find_call_expr(expr) if !Meta.isexpr(expr, :call) - throw(ArgumentError("Argument is not a :call, cannot parse it.")) + throw(ArgumentError("Couldn't find a :call Expr, are you documenting a function? If so this may be a bug in DocStringExtensions.jl, please open an issue and include the function being documented.")) end args = ASTArg[] From f1c9145f38d470d07201fbaa1e05565447d3b979 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Thu, 12 Sep 2024 14:54:03 +0200 Subject: [PATCH 10/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- src/parsing.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/parsing.jl b/src/parsing.jl index 1e1c789..bf107cb 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -19,6 +19,9 @@ function parse_arg_with_type(arg_expr::Expr) elseif n_expr_args == 2 # 'x::Int' ASTArg(; name=arg_expr.args[1], type=arg_expr.args[2]) + else + Meta.dump(arg_expr) + error("Couldn't parse typed argument (printed above)") end end @@ -85,7 +88,7 @@ function find_call_expr(obj) if Meta.isexpr(obj, :call) # Base case: we've found the :call expression return obj - elseif obj isa Symbol || (obj isa Expr && isempty(obj.args)) + elseif !(obj isa Expr) || isempty(obj.args) # Base case: this is the end of a branch in the expression tree return nothing end From 1efecf6a7792f1ea13b8b4f83cb4d491618ef08e Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Thu, 12 Sep 2024 16:01:24 +0200 Subject: [PATCH 11/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- src/utilities.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index fb37433..6f7a4d4 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -431,8 +431,14 @@ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Meth formatted_kws = format_args(kws, NTuple{length(kws), Any}, print_types) end - rt = Base.return_types(func, typesig) - can_print_rt = print_types && length(rt) >= 1 && rt[1] !== Nothing && rt[1] !== Union{} + rt = try + # We wrap this in a try-catch block because Base.return_types() is + # documented to fail on generated functions. + Base.return_types(func, typesig) + catch + nothing + end + can_print_rt = print_types && !isnothing(rt) && length(rt) >= 1 && rt[1] !== Nothing && rt[1] !== Union{} return printmethod_format(buffer, string(binding.var), formatted_args, formatted_kws; return_type = can_print_rt ? " -> $(rt[1])" : "") From 79783e4e0228fcd82a427a743d4c7c9d906b2d7d Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Mon, 16 Sep 2024 12:06:30 +0200 Subject: [PATCH 12/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9be2ee1..a4a71a8 100644 --- a/Project.toml +++ b/Project.toml @@ -6,7 +6,7 @@ version = "0.9.3" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" [compat] -CodeTracking = "1.3.6" +CodeTracking = "1" REPL = "1" julia = "1" From ebb95a95f677b0492e2700aa00e485c8daf27508 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Tue, 17 Sep 2024 12:07:50 +0200 Subject: [PATCH 13/13] fixup! Add support for default values to `TYPEDSIGNATURES` --- test/tests.jl | 61 ++++++++++++++++++++------------------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/test/tests.jl b/test/tests.jl index 64de8d6..5ef04bb 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -7,11 +7,26 @@ include("templates.jl") include("interpolation.jl") include("TestModule/M.jl") +if !isdefined(Base, :default_tt) + # This function isn't available in early Julia versions so we vendor it in + function default_tt(@nospecialize(f)) + ms = methods(f).ms + if length(ms) == 1 + return Base.tuple_type_tail(ms[1].sig) + else + return Tuple + end + end +else + import Base: default_tt +end + + # Helper function to get the Expr of a function. In some cases the argument # types will need to be explicitly given. function get_expr(f::Function, arg_types...) if isempty(arg_types) - arg_types = Base.default_tt(f) + arg_types = default_tt(f) end Meta.parse(code_string(f, arg_types)) @@ -168,15 +183,8 @@ end DSE.format(UntypedSignatures(g_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) - # On 1.10+, automatically generated methods have keywords in the metadata, - # hence the display difference between Julia versions. - if VERSION >= v"1.10" - @test occursin("\ng(; kwargs...)\n", str) - @test occursin("\ng(x=1; kwargs...)\n", str) - else - @test occursin("\ng()\n", str) - @test occursin("\ng()\n", str) - end + @test occursin("\ng(; kwargs...)\n", str) + @test occursin("\ng(x=1; kwargs...)\n", str) @test occursin("\n```\n", str) doc.data = Dict( @@ -187,17 +195,9 @@ end DSE.format(UntypedSignatures(g_expr), buf, doc) str = String(take!(buf)) @test occursin("\n```julia\n", str) - # On 1.10+, automatically generated methods have keywords in the metadata, - # hence the display difference between Julia versions. - if VERSION >= v"1.10" - @test occursin("\ng(; kwargs...)\n", str) - @test occursin("\ng(x=1; kwargs...)\n", str) - @test occursin("\ng(x=1, y=2; kwargs...)\n", str) - else - @test occursin("\ng()\n", str) - @test occursin("\ng(x=1)\n", str) - @test occursin("\ng(x=1, y=2)\n", str) - end + @test occursin("\ng(; kwargs...)\n", str) + @test occursin("\ng(x=1; kwargs...)\n", str) + @test occursin("\ng(x=1, y=2; kwargs...)\n", str) @test occursin("\ng(x=1, y=2, z=3; kwargs...)\n", str) @test occursin("\n```\n", str) @@ -281,21 +281,9 @@ end str = String(take!(buf)) @test occursin("\n```julia\n", str) if typeof(1) === Int64 - # On 1.10+, automatically generated methods have keywords in the metadata, - # hence the display difference between Julia versions. - if VERSION >= v"1.10" - @test occursin("\nh(x::Int64; kwargs...) -> Int64\n", str) - else - @test occursin("\nh(x::Int64) -> Int64\n", str) - end + @test occursin("\nh(x::Int64; kwargs...) -> Int64\n", str) else - # On 1.10+, automatically generated methods have keywords in the metadata, - # hence the display difference between Julia versions. - if VERSION >= v"1.10" - @test occursin("\nh(x::Int32; ...) -> Int32\n", str) - else - @test occursin("\nh(x::Int32) -> Int32\n", str) - end + @test occursin("\nh(x::Int32; ...) -> Int32\n", str) end @test occursin("\n```\n", str) @@ -416,11 +404,10 @@ end @test occursin("\n```julia\n", str) if VERSION >= v"1.6" && VERSION < v"1.7" @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer\n) -> Union{Nothing, Integer}\n", str) - @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer=zero(T)\n) -> Union{Nothing, Integer}\n", str) else @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer\n) -> Union{Nothing, T} where T<:Integer\n", str) - @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer=zero(T)\n) -> Union{Nothing, T} where T<:Integer\n", str) end + @test occursin("\nk_7(\n x::Union{Nothing, T} where T<:Integer,\n y::Integer=zero(T)\n) -> Union{Nothing, T} where T<:Integer\n", str) @test occursin("\n```\n", str) doc.data = Dict(