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

Add support for default values to TYPEDSIGNATURES #170

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ version = "0.9.3"
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"

[compat]
CodeTracking = "1"
REPL = "1"
julia = "1"

[extras]
CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Markdown", "Pkg", "Test"]
test = ["Markdown", "Pkg", "Test", "REPL", "CodeTracking"]
1 change: 1 addition & 0 deletions src/DocStringExtensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interpolation

# Includes.

include("parsing.jl")
include("utilities.jl")
include("abbreviations.jl")
include("templates.jl")
Expand Down
63 changes: 21 additions & 42 deletions src/abbreviations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -308,39 +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 end
const SIGNATURES = MethodSignatures(nothing, false)

"""
An [`Abbreviation`](@ref) for including a simplified representation of all the method
Expand All @@ -358,21 +331,25 @@ f(x::Int, y::Int; a, b...)
```
````
"""
const TYPEDSIGNATURES = TypedMethodSignatures()
const TYPEDSIGNATURES = MethodSignatures(nothing, true)

function format(ms::MethodSignatures, 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 = isnothing(ms.expr) ? nothing : parse_call(ms.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
Expand All @@ -395,9 +372,11 @@ function format(::TypedMethodSignatures, buf, doc)
else
t = tuples[findfirst(f, tuples)]
end
printmethod(buf, binding, func, method, t)

printmethod(buf, binding, func, method, ast_info, t, ms.print_types)
println(buf)
end

println(buf, "\n```\n")
end
end
Expand Down
126 changes: 126 additions & 0 deletions src/parsing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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 !Meta.isexpr(arg_expr, :(::))
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])
else
Meta.dump(arg_expr)
error("Couldn't parse typed argument (printed above)")
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an explicit failure branch here (even though we know that ::s always have one or two arguments) would be good.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added in f1c9145.

end

# Parse an argument with a default value.
# Example input: `x=5`
function parse_arg_with_default(arg_expr::Expr)
if !Meta.isexpr(arg_expr, :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 Meta.isexpr(arg_expr, :(::))
# With a type annotation
push!(list, parse_arg_with_type(arg_expr))
elseif Meta.isexpr(arg_expr, :kw)
# With a default value (and possibly a type annotation)
push!(list, parse_arg_with_default(arg_expr))
elseif Meta.isexpr(arg_expr, :parameters)
# Keyword arguments
parse_arglist!(arg_expr.args, args, kwargs, true)
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 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))
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. 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 Expr) || isempty(obj.args)
# Base case: this is the end of a branch in the expression tree
return nothing
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no case that covers any other potential types found in ASTs. This just assumes that it is now an Expr. Are we sure that that is a safe assumption to make?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, probably not. Should be fixed in f1c9145.


# 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
# 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 !Meta.isexpr(expr, :call)
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[]
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
13 changes: 8 additions & 5 deletions src/templates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -119,11 +125,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])
Loading
Loading