lifecycle provides a standard way to document the lifecycle stages of
functions and arguments, paired with tools to steer users away from
deprecated functions. Before we go in to the details, make sure that
you’re familiar with the lifecycle stages as described in
vignette("stages")
.
Basics
lifecycle badges make it easy for users to see the lifecycle stage
when reading the documentation. To use the badges, first call
usethis::use_lifecycle()
to embed the badge images in your
package (you only need to do this once), then use
lifecycle::badge()
to insert a badge:
#' `r lifecycle::badge("experimental")`
#' `r lifecycle::badge("deprecated")`
#' `r lifecycle::badge("superseded")`
Deprecated functions also need to advertise their status when run.
lifecycle provides deprecate_warn()
which takes three main
arguments:
The first argument,
when
, gives the version number when the deprecation occurred.The second argument,
what
, describes exactly what was deprecated.The third argument,
with
, provides the recommended alternative.
We’ll cover the details shortly, but here are a few sample uses:
lifecycle::deprecate_warn("1.0.0", "old_fun()", "new_fun()")
#> Warning: `old_fun()` was deprecated in lifecycle 1.0.0.
#> ℹ Please use `new_fun()` instead.
#> ℹ The deprecated feature was likely used in the base package.
#> Please report the issue to the authors.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
lifecycle::deprecate_warn("1.0.0", "fun()", "testthat::fun()")
#> Warning: `fun()` was deprecated in lifecycle 1.0.0.
#> ℹ Please use `testthat::fun()` instead.
#> ℹ The deprecated feature was likely used in the base package.
#> Please report the issue to the authors.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
lifecycle::deprecate_warn("1.0.0", "fun(old_arg)", "fun(new_arg)")
#> Warning: The `old_arg` argument of `fun()` is deprecated as of lifecycle 1.0.0.
#> ℹ Please use the `new_arg` argument instead.
#> ℹ The deprecated feature was likely used in the base package.
#> Please report the issue to the authors.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
(Note that the message includes the package name — this is automatically discovered from the environment of the calling function so will not work unless the function is called from the package namespace.)
The following sections describe how to use lifecycle badges and functions together to handle a variety of common development tasks.
Functions
Deprecate a function
First, add a badge to the the @description
block1. Briefly
describe why the deprecation occurred and what to use instead.
#' Add two numbers
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' This function was deprecated because we realised that it's
#' a special case of the [sum()] function.
Next, update the examples to show how to convert from the old usage to the new usage:
#' @examples
#' add_two(1, 2)
#' # ->
#' sum(1, 2)
Then add @keywords internal
to remove the function from
the documentation index. If you use pkgdown, also check that it’s no
longer listed in _pkgdown.yml
. These changes reduce the
chance of new users coming across a deprecated function, but don’t
prevent those who already know about it from referring to the docs.
#' @keywords internal
You’re now done with the docs, and it’s time to add a warning when
the user calls your function. Do this by adding call to
deprecate_warn()
on the first line of the function:
add_two <- function(x, y) {
lifecycle::deprecate_warn("1.0.0", "add_two()", "base::sum()")
x + y
}
add_two(1, 2)
#> Warning: `add_two()` was deprecated in lifecycle 1.0.0.
#> ℹ Please use `base::sum()` instead.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] 3
deprecate_warn()
generates user friendly messages for
two common deprecation alternatives:
Function in same package:
lifecycle::deprecate_warn("1.0.0", "fun_old()", "fun_new()")
Function in another package:
lifecycle::deprecate_warn("1.0.0", "old()", "package::new()")
For other cases, use the details
argument to provide
your own message to the user:
add_two <- function(x, y) {
lifecycle::deprecate_warn(
"1.0.0",
"add_two()",
details = "This function is a special case of sum(); use it instead."
)
x + y
}
add_two(1, 2)
#> Warning: `add_two()` was deprecated in lifecycle 1.0.0.
#> ℹ This function is a special case of sum(); use it instead.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] 3
It’s good practice to test that you’ve correctly implemented the
deprecation, testing that the deprecated function still works and that
it generates a useful warning. Using an expectation inside
testthat::expect_snapshot()
2 is a convenient way to
do this:
test_that("add_two is deprecated", {
expect_snapshot({
x <- add_two(1, 1)
expect_equal(x, 2)
})
})
If you have existing tests for the deprecated function you can
suppress the warning in those tests with the
lifecycle_verbosity
option:
test_that("add_two returns the sum of its inputs", {
withr::local_options(lifecycle_verbosity = "quiet")
expect_equal(add_two(1, 1), 2)
})
And then add a separate test specifically for the deprecation.
test_that("add_two is deprecated", {
expect_snapshot(add_two(1, 1))
})
Gradual deprecation
For particularly important functions, you can choose to add two other stages to the deprecation process:
deprecate_soft()
is used beforedeprecate_warn()
. This function only warns (a) users who try the feature from the global environment and (b) developers who directly use the feature (when running testthat tests). There is no warning when the deprecated feature is called indirectly by another package — the goal is to ensure that warn only the person who has the power to stop using the deprecated feature.deprecate_stop()
comes afterdeprecate_warn()
and generates an error instead of a warning. The main benefit over simply removing the function is that the user is informed about the replacement.
If you use these stages you’ll also need a process for bumping the deprecation stage for major and minor releases. We recommend something like this:
Search for
deprecate_stop()
and consider if you’re ready to the remove the function completely.Search for
deprecate_warn()
and replace withdeprecate_stop()
. Remove the remaining body of the function and any tests.Search for
deprecate_soft()
and replace withdeprecate_warn()
.
Rename a function
To rename a function without breaking existing code, move the implementation to the new function, then call the new function from the old function, along with a deprecation message:
#' Add two numbers
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' `add_two()` was renamed to `number_add()` to create a more
#' consistent API.
#' @keywords internal
#' @export
add_two <- function(foo, bar) {
lifecycle::deprecate_warn("1.0.0", "add_two()", "number_add()")
number_add(foo, bar)
}
# documentation goes here...
#' @export
number_add <- function(x, y) {
x + y
}
If you are renaming many functions as part of an API overhaul, it’ll often make sense to document all the changes in one file, like https://rvest.tidyverse.org/reference/rename.html.
Supersede a function
Superseding a function is simpler than deprecating it, since you don’t need to steer users away from it with a warning. So all you need to do is add a superseded badge:
#' Gather columns into key-value pairs
#'
#' @description
#' `r lifecycle::badge("superseded")`
Then describe why the function was superseded, and what the recommended alternative is:
#'
#' Development on `gather()` is complete, and for new code we recommend
#' switching to `pivot_longer()`, which is easier to use, more featureful,
#' and still under active development.
#'
#' In brief,
#' `df %>% gather("key", "value", x, y, z)` is equivalent to
#' `df %>% pivot_longer(c(x, y, z), names_to = "key", values_to = "value")`.
#' See more details in `vignette("pivot")`.
The rest of the documentation can stay the same.
If you’re willing to live on the bleeding edge of lifecycle, add a
call to the experimental signal_stage()
:
gather <- function(data, key = "key", value = "value", ...) {
lifecycle::signal_stage("superseded", "gather()")
}
This signal isn’t currently hooked up to any behaviour, but we plan to provide logging and analysis tools in a future release.
Mark function as experimental
To advertise that a function is experimental and the interface might change in the future, first add an experimental badge to the description:
#' @description
#' `r lifecycle::badge("experimental")`
If the function is very experimental, you might want to add
@keywords internal
too.
If you’re willing to try an experimental lifecycle feature, add a
call to signal_stage()
in the body:
cool_function <- function() {
lifecycle::signal_stage("experimental", "cool_function()")
}
This signal isn’t currently hooked up to any behaviour, but we plan to provide logging and analysis tools in a future release.
Arguments
Deprecate an argument, keeping the existing default
Take this example where we want to deprecate na.rm
in
favour of always making it TRUE.
add_two <- function(x, y, na.rm = TRUE) {
sum(x, y, na.rm = na.rm)
}
First, add a badge to the argument description:
#' @param na.rm `r lifecycle::badge("deprecated")` `na.rm = FALSE` is no
#' longer supported; this function will always remove missing values
And add a deprecation warning if na.rm
is FALSE. In this
case, there’s no replacement to the behaviour, so we instead use
details
to provide a custom message:
add_two <- function(x, y, na.rm = TRUE) {
if (!isTRUE(na.rm)) {
lifecycle::deprecate_warn(
when = "1.0.0",
what = "add_two(na.rm)",
details = "Ability to retain missing values will be dropped in next release."
)
}
sum(x, y, na.rm = na.rm)
}
add_two(1, NA, na.rm = TRUE)
#> [1] 1
add_two(1, NA, na.rm = FALSE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> ℹ Ability to retain missing values will be dropped in next release.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] NA
Deprecating an argument, providing a new default
Alternatively, you can change the default value to
lifecycle::deprecated()
to make the deprecation status more
obvious from the outside, and use lifecycle::is_present()
to test whether or not the argument was provided. Unlike
missing()
, this works for both direct and indirect
calls.
#' @importFrom lifecycle deprecated
add_two <- function(x, y, na.rm = deprecated()) {
if (lifecycle::is_present(na.rm)) {
lifecycle::deprecate_warn(
when = "1.0.0",
what = "add_two(na.rm)",
details = "Ability to retain missing values will be dropped in next release."
)
}
sum(x, y, na.rm = na.rm)
}
The chief advantage of this technique is that users will get a
warning regardless of what value of na.rm
they use:
add_two(1, NA, na.rm = TRUE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> ℹ Ability to retain missing values will be dropped in next release.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] 1
add_two(1, NA, na.rm = FALSE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> ℹ Ability to retain missing values will be dropped in next release.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] NA
Renaming an argument
You may want to rename an argument if you realise you have made a
mistake with the name of an argument. For example, you’ve realised that
an argument accidentally uses .
to separate a compound
name, instead of _
. You’ll need to temporarily permit both
arguments, generating a deprecation warning when the user supplies the
old argument:
add_two <- function(x, y, na_rm = TRUE, na.rm = deprecated()) {
if (lifecycle::is_present(na.rm)) {
lifecycle::deprecate_warn("1.0.0", "add_two(na.rm)", "add_two(na_rm)")
na_rm <- na.rm
}
sum(x, y, na.rm = na_rm)
}
add_two(1, NA, na.rm = TRUE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> ℹ Please use the `na_rm` argument instead.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] 1
Reducing allowed inputs to an argument
To narrow the set of allowed inputs, call
deprecate_warn()
only when the user supplies the previously
supported inputs. Make sure you preserve the previous behaviour:
add_two <- function(x, y) {
if (length(y) != 1) {
lifecycle::deprecate_warn("1.0.0", "foo(y = 'must be a scalar')")
y <- sum(y)
}
x + y
}
add_two(1, 2)
#> [1] 3
add_two(1, 1:5)
#> Warning: The `y` argument of `foo()` must be a scalar as of lifecycle 1.0.0.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
#> [1] 16
Anything else
You can wrap what
and with
in
I()
to deprecate behaviours not otherwise described
above:
lifecycle::deprecate_warn(
when = "1.0.0",
what = I('Setting the global option "pkg.opt" to "foo"')
)
#> Warning: Setting the global option "pkg.opt" to "foo" was deprecated in lifecycle
#> 1.0.0.
#> ℹ The deprecated feature was likely used in the base package.
#> Please report the issue to the authors.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
lifecycle::deprecate_warn(
when = "1.0.0",
what = I('The global option "pkg.another_opt"'),
with = I('"pkg.this_opt"')
)
#> Warning: The global option "pkg.another_opt" was deprecated in lifecycle 1.0.0.
#> ℹ Please use "pkg.this_opt" instead.
#> ℹ The deprecated feature was likely used in the base package.
#> Please report the issue to the authors.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
Note that your what
fragment needs to make sense with
“was deprecated …” added to the end, and your with
fragment
needs to make sense in the sentence “Please use {with}
instead”.