Skip to content

Commit

Permalink
Add ignore_kwargs to @attr macro (#1958)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgoettgens authored Jan 16, 2025
1 parent f7863c2 commit 67bfe48
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 2 deletions.
22 changes: 20 additions & 2 deletions src/Attributes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ end

"""
@attr RetType funcdef
@attr RetType ignore_kwargs=[...] funcdef
This macro is applied to the definition of a unary function, and enables
caching ("memoization") of its return values based on the argument. This
Expand All @@ -283,6 +284,10 @@ via [`get_attribute!`](@ref).
The name of the function is used as name for the underlying attribute.
In case that `funcdef` has keyword arguments that are not relevant for
attribute caching (e.g. `check::Bool=true`), these can be ignored by
putting them in the `ignore_kwargs` list.
Effectively, this turns code like this:
```julia
@attr RetType function myattr(obj::Foo)
Expand Down Expand Up @@ -324,9 +329,22 @@ julia> myattr(obj) # second time uses the cached result
```
"""
macro attr(rettype, expr::Expr)
return _attr_impl(__module__, __source__, expr, rettype, Symbol[])
end

macro attr(rettype, options, expr::Expr)
@assert options.head == :(=)
@assert length(options.args) == 2
@assert options.args[1] == :ignore_kwargs
@assert options.args[2].head == :vect
ignore_kwargs = Vector{Symbol}(options.args[2].args)
return _attr_impl(__module__, __source__, expr, rettype, ignore_kwargs)
end

function _attr_impl(__module__, __source__, expr::Expr, rettype, ignore_kwargs::Vector{Symbol})
d = MacroTools.splitdef(expr)
length(d[:args]) == 1 || throw(ArgumentError("Only unary functions are supported"))
length(d[:kwargs]) == 0 || throw(ArgumentError("Keyword arguments are not supported"))
length(setdiff(first.(MacroTools.splitarg.(d[:kwargs])), ignore_kwargs)) == 0 || throw(ArgumentError("non-ignored keyword arguments are not supported"))

# store the original function name
name = d[:name]
Expand All @@ -343,7 +361,7 @@ macro attr(rettype, expr::Expr)
wrapper_def = copy(d)
wrapper_def[:name] = name
wrapper_def[:body] = quote
return get_attribute!(() -> $(compute_name)($argname), $(argname), Symbol($(string(name))))::$(rettype)
return get_attribute!(() -> $(compute_name)($argname; $(first.(MacroTools.splitarg.(d[:kwargs]))...)), $(argname), Symbol($(string(name))))::$(rettype)
end
# insert the correct line number, so that `functionloc(name)` works correctly
wrapper_def[:body].args[1] = __source__
Expand Down
38 changes: 38 additions & 0 deletions test/Attributes-test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ A cached attribute.
my_derived_type(::Type{Tmp.Container{T}}) where T = T
@attr my_derived_type(T) cached_attr3(obj::T) where T <: Tmp.Container = obj.x

@attr Tuple{T,DataType,Vector{Any}} ignore_kwargs=[some_kwarg] cached_attr_with_kwarg1(obj::T; some_kwarg::Bool) where T = (obj,T,[])
@attr Tuple{T,DataType,Vector{Any}} ignore_kwargs=[some_kwarg] cached_attr_with_kwarg2(obj::T; some_kwarg::Bool=true) where T = (obj,T,[])

@testset "attribute caching for $T" for T in (Tmp.Foo, Tmp.Bar, Tmp.Quux, Tmp.FooBar{Tmp.Bar}, Tmp.FooBar{Tmp.Quux})

x = T()
Expand All @@ -211,6 +214,27 @@ my_derived_type(::Type{Tmp.Container{T}}) where T = T
y = @inferred cached_attr3(z)
@test y === x

# check case of ignored keyword arguments
x = T()
y = @inferred cached_attr_with_kwarg1(x; some_kwarg=true)
@test y == (x,T,[])
@test cached_attr_with_kwarg1(x; some_kwarg=true) === y
@test cached_attr_with_kwarg1(x; some_kwarg=false) === y

x = T()
y = @inferred cached_attr_with_kwarg2(x; some_kwarg=true)
@test y == (x,T,[])
@test cached_attr_with_kwarg2(x; some_kwarg=true) === y
@test cached_attr_with_kwarg2(x; some_kwarg=false) === y
@test cached_attr_with_kwarg2(x) === y

x = T()
y = @inferred cached_attr_with_kwarg2(x)
@test y == (x,T,[])
@test cached_attr_with_kwarg2(x; some_kwarg=true) === y
@test cached_attr_with_kwarg2(x; some_kwarg=false) === y
@test cached_attr_with_kwarg2(x) === y

# verify docstring is correctly attached
if VERSION >= v"1.12.0-DEV.1223"
@test string(@doc cached_attr) ==
Expand Down Expand Up @@ -249,6 +273,20 @@ end
@test_throws MethodError @macroexpand @attr Int foo(x::Int) = 1 Any
@test_throws MethodError @macroexpand @attr Int Int Int

# wrong handling of keyword arguments
@test_throws ArgumentError @macroexpand @attr Any foo(; some_kwarg::Bool) = 1
@test_throws ArgumentError @macroexpand @attr Any foo(; some_kwarg::Bool=true) = 1
@test_throws ArgumentError @macroexpand @attr Any foo(x::Int; some_kwarg::Bool) = 1
@test_throws ArgumentError @macroexpand @attr Any foo(x::Int; some_kwarg::Bool=true) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(; some_kwarg::Bool) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(; some_kwarg::Bool=true) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(x::Int; some_kwarg::Bool) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(x::Int; some_kwarg::Bool=true) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(; some_kwarg::Bool, other_kwarg::Int) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(; some_kwarg::Bool=true, other_kwarg::Int) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(x::Int; some_kwarg::Bool, other_kwarg::Int) = 1
@test_throws ArgumentError @macroexpand @attr Any ignore_kwargs=[other_kwarg] foo(x::Int; some_kwarg::Bool=true, other_kwarg::Int) = 1

# wrong kind of arguments
#@test_throws ArgumentError @macroexpand @attr Int Int
#@test_throws ArgumentError @macroexpand @attr foo(x::Int) = 1 Int
Expand Down

0 comments on commit 67bfe48

Please sign in to comment.