Usage of the lifecycle package

library(lifecycle)

Use lifecycle to document the status of your exported functions and arguments:

Stages

The lifecycle stages for functions and arguments are summarised in the figure below. They’re designed to closely mirror the lifecycle stages for packages.

There are two development stages.

The probable end state of most functions is stable:

Sometimes we are no longer certain that a feature follows or implements the right approach. In this case, we mark it as questioning. It can then either become superseded once we have implemented an alternative, or deprecated. The difference between superseded and deprecated is that a superseded feature is maintained indefinitely for backward compatibility. Alternatively, we may realise that a questioning feature is actually important, and move it back to stable. User feedback is usually important for taking these decisions and always welcome.

  1. Questioning The author is no longer convinced that the feature is the optimal approach. However, there are no recommended alternatives yet.

  2. Superseded (Previously called retired) A superseded feature is no longer under active development, and a known better alternative is available. However, it is indefinitely kept in the package for backward compatibility. The author will only make the necessary changes to ensure that the function continues working. No new features will be added, and only critical bugs will be fixed.

The deprecation lifecycle is divided into three stages in order to give time to users to switch to an alternative.

  1. Soft deprecated The author is no longer happy with a feature because they consider it sub-optimal compared to some other approach, or simply because they no longer have the time to maintain it. A soft-deprecated feature can still be used without hassle, but users should consider switching to an alternative approach.

  2. Deprecated The feature is likely to be discontinued in the next major release. Users should switch to an alternative approach as soon as possible.

  3. Defunct The feature can no longer be used. A defunct function is still exported, and a defunct argument is still part of the signature. This way an informative error can be thrown.

Finally, when a feature is no longer exposed or mentioned in the released version of the package, it is said to be archived.

Badges

Make sure your users know what stage a feature is by adding badges in the help topics of your functions.

badge

Verbosity of deprecation

lifecycle offers three levels of verbosity corresponding to the three deprecation stages.

Deprecating functions

These functions take the version number starting from which the feature is considered deprecated (it should remain the same across all deprecation stages), and a feature descriptor:

deprecate_warn("1.0.0", "mypkg::foo()")
#> Warning: `foo()` is deprecated as of mypkg 1.0.0.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_warnings()` to see where this warning was generated.

You can optionally provide a replacement:

deprecate_warn("1.0.0", "mypkg::foo()", "new()")
#> Warning: `foo()` is deprecated as of mypkg 1.0.0.
#> Please use `new()` instead.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_warnings()` to see where this warning was generated.

For the purpose of these examples we explicitly mentioned the namespace with mypkg::, however you can typically omit it because lifecycle infers the namespace from the calling environment. Specifying the namespace is mostly useful when the replacement is implemented in a different package.

# The new replacement
foobar_adder <- function(foo, bar) {
  foo + bar
}

# The old function still exported for compatibility
foobaz_adder <- function(foo, bar) {
  deprecate_warn("1.0.0", "foobaz_adder()", "foobar_adder()")
  foobar_adder(foo, bar)
}

Deprecating arguments

The syntax for deprecating argument is based on the syntax for deprecating functions:

deprecate_warn("1.0.0", "mypkg::foo(arg = )")
#> Warning: The `arg` argument of `foo()` is deprecated as of mypkg 1.0.0.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_warnings()` to see where this warning was generated.

deprecate_warn("1.0.0", "mypkg::foo(arg = )", "mypkg::foo(new = )")
#> Warning: The `arg` argument of `foo()` is deprecated as of mypkg 1.0.0.
#> Please use the `new` argument instead.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_warnings()` to see where this warning was generated.

An argument can be partially deprecated by disallowing certain input types:

deprecate_warn("1.0.0", "mypkg::foo(arg = 'must be a scalar integer')")
#> Warning: The `arg` argument of `foo()` must be a scalar integer as of mypkg 1.0.0.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_warnings()` to see where this warning was generated.

lifecycle also provides the deprecated() sentinel to use as default argument. This provides self-documentation for your users and makes it possible for external tools to determine which arguments are deprecated. Test whether the argument was supplied by the caller with lifecycle::is_present():

foobar_adder <- function(foo, bar, baz = deprecated()) {
  # Check if user has supplied `baz` instead of `bar`
  if (lifecycle::is_present(baz)) {

    # Signal the deprecation to the user
    deprecate_warn("1.0.0", "foobar_adder(baz = )", "foobar_adder(bar = )")

    # Deal with the deprecated argument for compatibility
    bar <- baz
  }

  foo + bar
}

Workflow

Where do these deprecation warnings come from?

Call lifecycle::last_warnings() to see backtraces for all the deprecation warnings that were issued during the last top-level command.

Bumping deprecation stage

Some manual search and replace is needed to bump the status of deprecated features. We recommend starting with defunct features and work your way up:

  1. Search for deprecate_stop() and remove the feature from the package. The feature is now archived.

  2. Search for deprecate_warn() and replace with deprecate_stop().

  3. Search for deprecate_soft() and replace with deprecate_warn().

  4. Call deprecate_soft() from newly deprecated functions.

Don’t forget to update the badges in the documentation topics.

Find out what deprecated features you rely on

Test whether your package depends on deprecated features directly or indirectly by setting the verbosity option in the tests/testthat.R file just before test_check() is called:

library(testthat)
library(mypackage)

options(lifecycle_verbosity = "error")
test_check("mypackage")

This forces all deprecated features to fail. You can also set the relevant options manually to force warnings or errors in your session:

# Force silence
options(lifecycle_verbosity = "quiet")

# Force warnings
options(lifecycle_verbosity = "warning")

# Force errors
options(lifecycle_verbosity = "error")

Forcing warnings can be useful in conjuction with last_warnings(), which prints backtraces for all the deprecation warnings issued during the last top-level command.

Test deprecated features

Test whether a deprecated feature still works by setting lifecycle_verbosity to "quiet":

test_that("`baz` argument of `foobar_adder()` still works", {
  withr::local_options(list(lifecycle_verbosity = "quiet"))
  foobar_adder(1, baz = 2)
})

You can also set up verbosity for a whole testthat file within setup() and teardown() blocks:

setup(options(lifecycle_verbosity = "quiet"))
teardown(options(lifecycle_verbosity = NULL))

Test that a feature is correctly deprecated with expect_deprecated() or expect_defunct():

test_that("`baz` argument of `foobar_adder()` is deprecated", {
  expect_deprecated(foobar_adder(1, baz = 2))
})

test_that("`foo()` is defunct", {
  expect_defunct(foo())
})

More control over verbosity can be exercised with the lifecycle_verbosity option. See ?verbosity.