rlang icon indicating copy to clipboard operation
rlang copied to clipboard

add combination of `is_installed()` and `check_installed()`

Open maxheld83 opened this issue 1 year ago • 4 comments

I sometimes find myself in this situation:

if (rlang::is_installed("optional-package")) {
  # do something more fully-featured
} else {
  # do a simpler version without the opt package
}

Typically, such an optional-package will be a Suggests:.

If they're missing it, I'd like to inform my users about them missing optional-package and (once) prompt them to install it. rlang::check_installed() is great for this except that it errors out.

So I'd like to us a "combination" (loosely speaking) of the two functions which:

  • always returns as a predicate would (TRUE/FALSE)
  • if interactive, prompt the user to install
  • if non-interactive inform the user that they're missing

If you deem this useful, read on -- and I'll be happy to write up a PR with some guidance --, otherwise feel free to close 🙂

I've build a hacky version of this (see below), but ran into these limitations which may require/justify inclusion/deeper integration in rlang:

  • just catching and re-signaling (as a message) check_installed() does not work great, because:
    • the wording (~ "required package missing") of the condition doesn't quite work as a message; it sounds too alarming and may confuse users.
    • I can't use .frequency for how often to even prompt the user (which may be even more annoying than messages, if done too often). (related #1729)
  • rebuilding based off of only is_installed() would duplicate a lot of work already done in rlang.

In summary, it might be nice to include this feature in rlang, though to avoid code duplication, it might require some refactoring.

Also some unresolved questions:

  • [ ] how should this function be named? It's too side-effecty for is_installed(). Something like is_installed_after_trying()? Is there a idiomatic name for this kind of pattern?
  • [ ] is it ok to reuse rlang:::needs_signal() for when to try installing? That clearly needs a .frequency not to be annoying, rlang:::needs_signal() isn't about when/how often to prompt.

maxheld83 avatar Jul 06 '24 17:07 maxheld83

I'm running into the exact same scenario. I couldn't for the life of me figure out how to combine try_fetch() + check_installed() in a way that would produce the desired behaviour.

teunbrand avatar Jul 11 '24 08:07 teunbrand

@teunbrand in case that's helpful I got this to work:

#' Checks if a package is installed and *informs* the user if not
#'
#' This is wrapper around [rlang::check_installed];
#' instead of erroring out if the check fails it returns `FALSE`.
#' However, unlike [rlang::is_installed], it emits a message to the user.
#'
#' @inheritParams rlang::check_installed
#' @inheritDotParams rlang::check_installed
#' @example inst/examples/dependencies/is_installed2/missing.R
#' @example inst/examples/dependencies/is_installed2/present.R
#' @keywords dependencies helper
#' @export
is_installed2 <- function(...) {
  if (rlang::is_installed(...)) {
    return(TRUE)
  }
  rlang::try_fetch(
    # TODO this should only interact with the user as per .frequency
    # might get annoying otherwise
    # but that is blocked by deep integration in rlang
    rlang::check_installed(...),
    error = function(cnd) {
      inform_missing_pkgs(...)
    }
  )
  rlang::is_installed(...)
}

you can replace inform_missing_pkgs() with your own simpler rlang::inform("blah").

Full source, largely copied from rlang itself is here: https://github.com/dataheld/elf/blob/main/R/dependencies.R

Though it's all a bit hacky, not ready for prime time.

maxheld83 avatar Jul 11 '24 09:07 maxheld83

Thanks for sharing this code, Max! This is similar to what I attempted but I found one gripe with this approach. If a user selects 'No' to a prompt, the function should return FALSE but instead no value is returned at all. It is hard to provide a reprex as this only occurs in interactive sessions, but essentially:

x <- is_installed2("foobar")
# User should select 'No' as answer
print(x)
#> Error: object 'x' not found

teunbrand avatar Jul 11 '24 09:07 teunbrand

Related https://github.com/r-lib/rlang/issues/1658. In dm there is a standalone file that basically extends the is_installed() behavior to skip in tests.

olivroy avatar Jul 11 '24 19:07 olivroy