From 67bfe4885932e593907751514d942d4b7c50ba0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20G=C3=B6ttgens?= Date: Thu, 16 Jan 2025 10:54:29 +0100 Subject: [PATCH] Add `ignore_kwargs` to `@attr` macro (#1958) --- src/Attributes.jl | 22 ++++++++++++++++++++-- test/Attributes-test.jl | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Attributes.jl b/src/Attributes.jl index c2d1f3cac1..386a72a785 100644 --- a/src/Attributes.jl +++ b/src/Attributes.jl @@ -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 @@ -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) @@ -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] @@ -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__ diff --git a/test/Attributes-test.jl b/test/Attributes-test.jl index e21e523039..6da35149c1 100644 --- a/test/Attributes-test.jl +++ b/test/Attributes-test.jl @@ -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() @@ -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) == @@ -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