-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathchanges-multivers.qmd
163 lines (117 loc) · 6.62 KB
/
changes-multivers.qmd
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
# Work with multiple dependency versions {#sec-changes-multivers}
```{r}
#| include = FALSE
source("common.R")
```
## What's the pattern?
In an ideal world, when a dependency of your package changes its interface, you want your package to work with both versions.
This is more work but it has two significant advantages:
- The CRAN submission process is decoupled.
If your package only works with the development version of a dependency, you'll need to carefully coordinate your CRAN submission with the dependencies CRAN submission.
If your package works with both versions, you can submit first, making life easier for CRAN and for the maintainer of the dependency.
- User code is less likely to be affected.
If your package only works with the latest version of the dependency, then when a user upgrades your package, the dependency also must update.
Upgrading multiple packages is more likely to affect user code than updating a single package.
In this pattern, you'll learn how to write code designed to work with multiple versions of a dependency, and you'll how to adapt your existing Travis configuration to test that you've got it right.
## Writing code
Sometimes there will be an easy way to change the code to work with both old and new versions of the package; do this if you can!
However, in most cases, you can't, and you'll need an `if` statement that runs different code for new and old versions of the package:
```{r}
#| eval = FALSE
if (dependency_has_new_interface()) {
# freshly written code that works with in-development dependency
} else {
# existing code that works with the currently released dependency
}
```
(If your freshly written code uses functions that don't exist in the CRAN version this will generate an R CMD check `NOTE` when you submit it to CRAN. This is one of the few NOTEs that you can explain: just mention that it's needed for forward/backward compatibility in your submission notes.)
We recommend always pulling out the check out into a function so that the logic lives in one place.
This will make it much easier to pull it out when it's no longer needed, and provides a good place to document why it's needed.
There are three basic approaches to implement `dependency_has_new_interface()`:
- Check the version of the package.
This is recommended in most cases, but requires that the dependency author use a specific version convention.
- Check for existence of a function.
- Check for a specific argument value, or otherwise detect that the interface has changed.
### Case study: tidyr
To make the problem concrete so we can show of some real code, lets imagine we have a package that uses `tidyr::nest()`.
`tidyr::nest()` changed substantially between 0.8.3 and 1.0.0, and so we need to write code like this:
```{r}
#| eval = FALSE
if (tidyr_new_interface()) {
out <- tidyr::nest_legacy(df, x, y, z)
} else {
out <- tidyr::nest(df, c(x, y, z))
}
```
(As described above, when submitted to CRAN this will generate a note about missing `tidyr::nest_legacy()` which can be explained in the submission comments.)
To implement `tidyr_new_interface()`, we need to think about three versions of tidyr:
- 0.8.3: the version currently on CRAN with the old interface.
- 0.8.99.9000: the development version with the new interface.
As usualy, the fourth component is \>= 9000 to indicate that it's a development version.
Note, however, that the patch version is 99; this indicates that release includes breaking changes.
- 1.0.0: the future CRAN version; this is the version that will be submitted to CRAN.
The main question is how to write `tidyr_new_interface()`.
There are three options:
- Check that the version is greater than the development version:
```{r}
tidyr_new_interface <- function() {
packageVersion("tidyr") > "0.8.99"
}
```
This technique works because tidyr uses the convention that the development version of backward incompatible functions contain `99` in the third (patch) component.
- If tidyr didn't adopt this naming convention, we could test for the existence of `unnest_legacy()`.
```{r}
tidyr_new_interface1 <- function() {
exists("unnest_legacy", asNamespace("tidyr"))
}
```
- If the interface change was more subtle, you might have to think more creatively.
If the package uses the [lifecycle](http://lifecycle.r-lib.org/) system, one approach would be to test for the presence of `deprecated()` in the function arguments:
```{r}
tidyr_new_interface2 <- function() {
identical(formals(tidyr::unnest)$.drop, quote(deprecated()))
}
```
All these approaches are reasonably fast, so it's unlikely they'll have any impact on performance unless called in a very tight loop.
```{r}
bench::mark(
version = tidyr_new_interface(),
exists = tidyr_new_interface1(),
formals = tidyr_new_interface2()
)[1:5]
```
If you do need to use `packageVersion()` inside a performance sensitive function, I recommend caching the result in `.onLoad()` (which, by convention, lives in `zzz.R`).
There a few ways to do this; but the following block shows one approach that matches the function interface I used above:
```{r}
tidyr_new_interface <- function() FALSE
.onLoad <- function(...) {
if (utils::packageVersion("tidyr") > "0.8.2") {
tidyr_new_interface <<- function() TRUE
}
}
```
## Testing with multiple package versions
It's good practice to test both old and new versions of the code, but this is challenging because you can't both sets of tests in the same R session.
The easiest way to make sure that both versions are work and stay working is to use Travis.
Before the dependency is released, you can manually install the development version using `remotes::install_github()`:
``` yaml
matrix:
include:
- r: release
name: tidyr-devel
before_script: Rscript -e "remotes::install_github('tidyverse/tidyr')"
```
It's not generally that important to check that your code continues to work with an older version of the package, but if you want to you can use `remotes::install_version()`:
``` yaml
matrix:
include:
- r: release
name: tidyr-0.8
before_script: Rscript -e "remotes::install_version('tidyr', '0.8.3')"
```
## Using only the new version
At some point in the future, you'll decide that the old version of the package is no longer widely used and you want to simplify your package by only depending on the new version.
There are three steps:
- In the DESCRIPTION, bump the required version of the dependency.
- Search for `dependency_has_new_interface()`; remove the function definition and all uses (retaining the code used with the new version).
- Remove the additional build in `.travis.yml`.