-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathcommunicate.Rmd
387 lines (289 loc) · 12.1 KB
/
communicate.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
---
title: "Communicate lifecycle changes in your functions"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Communicate lifecycle changes in your functions}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
options(
# Pretend we're in the lifecycle package
"lifecycle:::calling_package" = "lifecycle",
# suppress last_lifecycle_warnings() message by default
"lifecycle_verbosity" = "warning"
)
```
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, eval = FALSE}
#' `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:
```{r}
lifecycle::deprecate_warn("1.0.0", "old_fun()", "new_fun()")
lifecycle::deprecate_warn("1.0.0", "fun()", "testthat::fun()")
lifecycle::deprecate_warn("1.0.0", "fun(old_arg)", "fun(new_arg)")
```
(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` block[^1].
Briefly describe why the deprecation occurred and what to use instead.
[^1]: We only use an explicit `@description` when the description will be multiple paragraphs, as in these examples.
```{r}
#' 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:
```{r}
#' @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.
```{r}
#' @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:
```{r}
add_two <- function(x, y) {
lifecycle::deprecate_warn("1.0.0", "add_two()", "base::sum()")
x + y
}
add_two(1, 2)
```
`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:
```{r}
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)
```
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:
[^2]: You can learn more about snapshot testing in `vignette("snapshotting", package = "testthat")`.
```{r, eval = FALSE}
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:
```{r, eval = FALSE }
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.
```{r, eval = FALSE}
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 before `deprecate_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 after `deprecate_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:
1. Search for `deprecate_stop()` and consider if you're ready to the remove the function completely.
2. Search for `deprecate_warn()` and replace with `deprecate_stop()`.
Remove the remaining body of the function and any tests.
3. Search for `deprecate_soft()` and replace with `deprecate_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:
```{r}
#' 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:
```{r}
#' Gather columns into key-value pairs
#'
#' @description
#' `r lifecycle::badge("superseded")`
```
Then describe why the function was superseded, and what the recommended alternative is:
```{r}
#'
#' 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()`:
```{r}
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:
```{r}
#' @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:
```{r}
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.`
```{r}
add_two <- function(x, y, na.rm = TRUE) {
sum(x, y, na.rm = na.rm)
}
```
First, add a badge to the argument description:
```{r}
#' @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:
```{r}
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)
add_two(1, NA, na.rm = FALSE)
```
### 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.
```{r}
#' @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:
```{r}
add_two(1, NA, na.rm = TRUE)
add_two(1, NA, na.rm = FALSE)
```
### 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:
```{r}
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)
```
### 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:
```{r}
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)
add_two(1, 1:5)
```
## Anything else
You can wrap `what` and `with` in `I()` to deprecate behaviours not otherwise described above:
```{r}
lifecycle::deprecate_warn(
when = "1.0.0",
what = I('Setting the global option "pkg.opt" to "foo"')
)
lifecycle::deprecate_warn(
when = "1.0.0",
what = I('The global option "pkg.another_opt"'),
with = I('"pkg.this_opt"')
)
```
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".