[ANN] DeprecateKeywords.jl – a macro for keyword deprecation

DeprecateKeywords.jl

DeprecateKeywords.jl is a tiny package (77 lines) which defines a macro @depkws for keyword deprecation. (I wasn’t sure if I should even post this since it’s such a tiny macro, but I thought I might as well.)

While normally you can use Base.@deprecate for deprecating functions and arguments, because multiple dispatch does not apply to keywords, you actually need to manipulate the original function signature.

For example, let’s say we wish to deprecate the keyword old_kw1 in favor of new_kw1, and
and old_kw2 in favor of new_kw2:

using DeprecateKeywords

@depkws function foo(; new_kw1=2, new_kw2=3,
                       @deprecate(old_kw1, new_kw1),
                       @deprecate(old_kw2, new_kw2))
    return new_kw1 + new_kw2
end

The use of normal @deprecate in here is syntactic sugar to help make the signature more intuitive. The @depkws will simply consume the @deprecates and and interpret their contents.

With this, we can use both the old and new keywords. If using the old keyword, it will automatically be passed to the new keyword, but with a deprecation warning.

julia> foo(new_kw1=1, new_kw2=2)
3

julia> foo(old_kw1=1, new_kw2=2)
┌ Warning: Keyword argument `old_kw1` is deprecated. Use `new_kw1` instead.
│   caller = top-level scope at REPL[5]:1
└ @ Core REPL[5]:1
3

(The warning uses depwarn, so is only visible if one starts with --depwarn=yes)

Here’s what this actually gets expanded to:

function foo(; old_kw2 = DeprecatedDefault, old_kw1 = DeprecatedDefault, new_kw1 = begin
                  if old_kw1 !== DeprecatedDefault
                      Base.depwarn("Keyword argument `old_kw1` is deprecated. Use `new_kw1` instead.", :foo)
                      old_kw1
                  else
                      2
                  end
              end, new_kw2 = begin
                  if old_kw2 !== DeprecatedDefault
                      Base.depwarn("Keyword argument `old_kw2` is deprecated. Use `new_kw2` instead.", :foo)
                      old_kw2
                  else
                      3
                  end
              end)
      new_kw1 + new_kw2
  end

(i.e., it’s not doing anything sophisticated and 99% of the time you could just do this by hand. I basically just wanted a macro to help lower the mental barrier for keyword deprecation for my libraries.)


Other notes/warnings

  • I’m not 100% sure if/when this might prevent Julia from specializing to types, or if it is different from how you would set this up manually. So just be wary of major type inference issues when passing the deprecated keywords.
  • This does not check whether the user passes both keyword arguments. (It might be better to use kws... and then pass through the old keywords within the function body. I didn’t do this in my current approach so that the user could still use kws... in the signature if they wish.)
  • This uses the very nice MacroTools.jl package to help make the macro generic.

Contributions very much appreciated. I’m also open to better syntax suggestions!


Edits:

  • Changed default value for deprecated keywords to a custom symbol to avoid value collisions (Thanks @Ronis_BR)
12 Likes

Awesome! I had to do some strange things in PrettyTables.jl to deprecate KW arguments:

I looked very briefly to your expanded code and I have one question: what happened if the deprecated keyword is passed with the value nothing? For example, we can call pretty_tables with hlines = nothing to remove all horizontal lines. Would this package works if I try to deprecate hlines and the user assigns nothing to it?

1 Like

Great point. Indeed in that case, the hlines=nothing example would not work. To fix this, perhaps I should create a new type to stand-in as a default that is guaranteed not to collide with other defaults? Something like

abstract type DeprecatedDefault end

function foo(; old_kw2 = DeprecatedDefault, old_kw1 = DeprecatedDefault, new_kw1 = begin
                  if old_kw1 !== DeprecatedDefault
                      Base.depwarn("Keyword argument `old_kw1` is deprecated. Use `new_kw1` instead.", :foo)
                      old_kw1
                  else
                      2
                  end
              end, new_kw2 = begin
                  if old_kw2 !== DeprecatedDefault
                      Base.depwarn("Keyword argument `old_kw2` is deprecated. Use `new_kw2` instead.", :foo)
                      old_kw2
                  else
                      3
                  end
              end)
      new_kw1 + new_kw2
  end

Thoughts?

1 Like

Fixed!

Thanks @Ronis_BR for bringing this up

1 Like

Perfect! Thanks @MilesCranmer !

1 Like