Skip to content

Commit ea154c3

Browse files
authored
Merge pull request #1199 from lionel-/pnf-error
Add restart for package not found errors
2 parents 365b986 + ea90d07 commit ea154c3

File tree

5 files changed

+225
-28
lines changed

5 files changed

+225
-28
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ S3method(c,rlang_envs)
3636
S3method(cnd_body,default)
3737
S3method(cnd_footer,default)
3838
S3method(cnd_header,default)
39+
S3method(cnd_header,rlib_error_package_not_found)
3940
S3method(conditionMessage,rlang_error)
4041
S3method(format,rlang_error)
4142
S3method(format,rlang_trace)

NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# rlang (development version)
22

3+
* `check_installed()` now consults the
4+
`rlib_restart_package_not_found` global option to determine whether
5+
to prompt users to install packages. This also disables the restart
6+
mechanism (see below).
7+
8+
* `check_installed()` now signals errors of class
9+
`rlib_error_package_not_found` with a
10+
`rlib_restart_package_not_found` restart. This allows calling
11+
handlers to install the required packages and restart the check
12+
(#1150).
13+
314
* New `rlib_bytes` class imported from the bench package (#1117).
415
It prints and parses human-friendly sizes.
516

R/session.R

Lines changed: 122 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
#' interactive or if the user chooses not to install the packages,
1717
#' the current evaluation is aborted.
1818
#'
19+
#' You can disable the prompt by setting the
20+
#' `rlib_restart_package_not_found` global option to `FALSE`. In that
21+
#' case, missing packages always cause an error.
22+
#'
1923
#' @param pkg The package names.
2024
#' @param reason Optional string indicating why is `pkg` needed.
2125
#' Appears in error messages (if non-interactive) and user prompts
@@ -27,6 +31,24 @@
2731
#' provided in `pkg` are installed, `FALSE`
2832
#' otherwise. `check_installed()` either doesn't return or returns
2933
#' `NULL`.
34+
#'
35+
#' @section Handling package not found errors:
36+
#' `check_installed()` signals error conditions of class
37+
#' `rlib_error_package_not_found`. The error includes `pkg` and
38+
#' `version` fields. They are vectorised and may include several
39+
#' packages.
40+
#'
41+
#' The error is signalled with a `rlib_restart_package_not_found`
42+
#' restart on the stack to allow handlers to install the required
43+
#' packages. To do so, add a [calling handler][withCallingHandlers]
44+
#' for `rlib_error_package_not_found`, install the required packages,
45+
#' and invoke the restart without arguments. This restarts the check
46+
#' from scratch.
47+
#'
48+
#' The condition is not signalled in non-interactive sessions, in the
49+
#' restarting case, or if the `rlib_restart_package_not_found` user
50+
#' option is set to `FALSE`.
51+
#'
3052
#' @export
3153
#' @examples
3254
#' is_installed("utils")
@@ -35,6 +57,12 @@
3557
is_installed <- function(pkg, ..., version = NULL) {
3658
check_dots_empty0(...)
3759

60+
# Internal mechanism for unit tests
61+
hook <- peek_option("rlang:::is_installed_hook")
62+
if (is_function(hook)) {
63+
return(all(hook(pkg, version)))
64+
}
65+
3866
if (!all(map_lgl(pkg, function(x) is_true(requireNamespace(x, quietly = TRUE))))) {
3967
return(FALSE)
4068
}
@@ -71,50 +99,48 @@ check_installed <- function(pkg,
7199
needs_install <- !map2_lgl(pkg, version, function(p, v) is_installed(p, version = v))
72100
}
73101

74-
if (!any(needs_install)) {
102+
missing_pkgs <- pkg[needs_install]
103+
missing_vers <- version[needs_install]
104+
105+
if (!length(missing_pkgs)) {
75106
return(invisible(NULL))
76107
}
77108

78-
missing_pkgs <- pkg[needs_install]
79-
missing_pkgs <- chr_quoted(missing_pkgs)
109+
cnd <- new_error_package_not_found(
110+
missing_pkgs,
111+
missing_vers,
112+
reason = reason
113+
)
80114

81-
if (!is_null(version)) {
82-
missing_vers <- version[needs_install]
83-
missing_pkgs <- map2_chr(missing_pkgs, missing_vers, function(p, v) {
84-
if (is_na(v)) {
85-
p
86-
} else {
87-
paste0(p, " (>= ", v, ")")
88-
}
89-
})
115+
restart <- peek_option("rlib_restart_package_not_found") %||% TRUE
116+
if (!is_bool(restart)) {
117+
abort("`rlib_restart_package_not_found` must be a logical value.")
90118
}
91119

92-
missing_pkgs_enum <- chr_enumerate(missing_pkgs, final = "and")
120+
if (!is_interactive() || !restart) {
121+
abort(cnd_header(cnd))
122+
}
93123

94-
n <- length(missing_pkgs)
95-
info <- pluralise(
96-
n,
97-
paste0("The package ", missing_pkgs_enum, " is required"),
98-
paste0("The packages ", missing_pkgs_enum, " are required")
99-
)
100-
if (is_null(reason)) {
101-
info <- paste0(info, ".")
102-
} else {
103-
info <- paste(info, reason)
124+
if (signal_package_not_found(cnd)) {
125+
# A calling handler asked for a restart. Disable restarts and try
126+
# again.
127+
return(with_options(
128+
"rlib_restart_package_not_found" = FALSE,
129+
check_installed(pkg, reason, version = version)
130+
))
104131
}
105132

133+
header <- cnd_header(cnd)
134+
135+
n <- length(missing_pkgs)
106136
question <- pluralise(
107137
n,
108138
"Would you like to install it?",
109139
"Would you like to install them?"
110140
)
111141

112-
if (!is_interactive()) {
113-
abort(info)
114-
}
115-
116142
cat(paste_line(
117-
paste0(info(), " ", info),
143+
paste0(info(), " ", header),
118144
paste0(cross(), " ", question),
119145
.trailing = TRUE
120146
))
@@ -128,3 +154,71 @@ check_installed <- function(pkg,
128154
utils::install.packages(missing_pkgs)
129155
}
130156
}
157+
158+
new_error_package_not_found <- function(pkg,
159+
version = NULL,
160+
...,
161+
reason = NULL,
162+
class = NULL) {
163+
if (!is_character(pkg)) {
164+
abort("`pkg` must be character vector.")
165+
}
166+
if (!length(pkg)) {
167+
abort("`pkg` must contain at least one package.")
168+
}
169+
if (!is_null(version) && !is_character(version, n = length(pkg))) {
170+
abort("`version` must be a character vector as long as `pkg`.")
171+
}
172+
173+
error_cnd(
174+
class = c(class, "rlib_error_package_not_found"),
175+
pkg = pkg,
176+
version = version,
177+
reason = reason,
178+
...
179+
)
180+
}
181+
182+
#' @export
183+
cnd_header.rlib_error_package_not_found <- function(cnd, ...) {
184+
pkg <- cnd$pkg
185+
version <- cnd$version
186+
reason <- cnd$reason
187+
n <- length(pkg)
188+
189+
pkg_enum <- chr_quoted(cnd$pkg)
190+
191+
if (!is_null(version)) {
192+
pkg_enum <- map2_chr(pkg_enum, version, function(p, v) {
193+
if (is_na(v)) {
194+
p
195+
} else {
196+
paste0(p, " (>= ", v, ")")
197+
}
198+
})
199+
}
200+
201+
pkg_enum <- chr_enumerate(pkg_enum, final = "and")
202+
203+
info <- pluralise(
204+
n,
205+
paste0("The package ", pkg_enum, " is required"),
206+
paste0("The packages ", pkg_enum, " are required")
207+
)
208+
209+
if (is_null(reason)) {
210+
paste0(info, ".")
211+
} else {
212+
paste(info, reason)
213+
}
214+
}
215+
216+
signal_package_not_found <- function(cnd) {
217+
withRestarts({
218+
signalCondition(cnd)
219+
FALSE
220+
},
221+
rlib_restart_package_not_found = function() {
222+
TRUE
223+
})
224+
}

man/is_installed.Rd

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-session.R

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,71 @@ test_that("check_installed() checks minimal versions", {
3636
(expect_error(check_installed(c("_foo", "_bar"), "to proceed.", version = c("1.0", "2.0"))))
3737
})
3838
})
39+
40+
test_that("pnf error is validated", {
41+
expect_pnf <- function(out, pkg, ver) {
42+
expect_s3_class(out, "rlib_error_package_not_found")
43+
expect_equal(out$pkg, pkg)
44+
expect_equal(out$version, ver)
45+
}
46+
expect_pnf(new_error_package_not_found("foo"), "foo", NULL)
47+
expect_pnf(new_error_package_not_found("foo", "1.0"), "foo", "1.0")
48+
expect_pnf(new_error_package_not_found(c("foo", "bar"), c("1.0", "1.0")), c("foo", "bar"), c("1.0", "1.0"))
49+
50+
expect_error(
51+
new_error_package_not_found(chr()),
52+
"at least one package"
53+
)
54+
expect_error(
55+
new_error_package_not_found(c("foo", "bar"), "1.0"),
56+
"as long as `pkg`"
57+
)
58+
})
59+
60+
test_that("can handle check-installed", {
61+
local_interactive()
62+
63+
# Override `is_installed()` results
64+
override <- NULL
65+
is_installed_hook <- function(pkg, ver) {
66+
if (is_bool(override)) {
67+
rep_along(pkg, override)
68+
} else {
69+
with_options(
70+
"rlang:::is_installed_hook" = NULL,
71+
is_installed(pkg, version = ver)
72+
)
73+
}
74+
}
75+
local_options("rlang:::is_installed_hook" = is_installed_hook)
76+
77+
test_env <- current_env()
78+
handle <- function(value, frame, expr) {
79+
withCallingHandlers(
80+
rlib_error_package_not_found = function(cnd) {
81+
override <<- value
82+
invokeRestart("rlib_restart_package_not_found")
83+
},
84+
expr
85+
)
86+
}
87+
88+
override <- NULL
89+
expect_no_error(
90+
handle(
91+
TRUE,
92+
test_env,
93+
check_installed(c("foo", "bar"), version = c("1.0", "2.0"))
94+
)
95+
)
96+
97+
override <- NULL
98+
expect_error(
99+
handle(
100+
FALSE,
101+
test_env,
102+
check_installed(c("foo", "bar"), version = c("1.0", "2.0"))
103+
),
104+
"are required"
105+
)
106+
})

0 commit comments

Comments
 (0)