In R, how can I check for the existence of a function in an unloaded package?
Asked Answered
P

6

6

I can check that a function exists in my environment:

> exists("is.zoo")
[1] FALSE

I can check that a function exists after loading a package by loading it:

> library(zoo)
> exists("is.zoo")
[1] TRUE

But how can I check that a function exists in a package without loading that package?

> exists("zoo::is.zoo")
[1] FALSE
Piggin answered 28/7, 2015 at 10:54 Comment(5)
I believe you need to look into the package source. Exported functions you often can find easily in the NAMESPACE file, but usually you should find them also in the package's documentation. Otherwise you need to check the actual source files.Excommunicatory
I need to be able to check programmatically.Piggin
Why do you need to check this programmatically?Excommunicatory
You could cheat by writing a function which loads the library, checks for the function, then unloads the library and returns the existence value.Brown
Not at a machine w/ R installed right now, so can't test, but maybe playing with the ::: operator to search NAMESPACE might cover most cases.Brown
P
1

You can use the exists function to look inside namespaces:

exists2 <- function(x) {

    assertthat::assert_that(assertthat::is.string(x))

    split <- base::strsplit(x, "::")[[1]]

    if (length(split) == 1) {
        base::exists(split[1])
    } else if (length(split) == 2) {
        base::exists(split[2], envir = base::asNamespace(split[1]))
    } else {
        stop(paste0("exists2 cannot handle ", x))
    }
}
Piggin answered 28/7, 2015 at 12:0 Comment(0)
A
6

You can view the source of a function even if its not loaded.

> exists("zoo::is.zoo")
[1] FALSE
> zoo::is.zoo
function (object)
inherits(object, "zoo")
<environment: namespace:zoo>

So you could exploit that with a function like this

exists_unloaded <- function(fn) {
  tryCatch( {
    fn
    TRUE
  }, error=function(e) FALSE
  )
}

If the call to fn errors, it'll return FALSE; if fn shows the source, the TRUE will be returned.

> exists("zoo::is.zoo")
[1] FALSE
> exists_unloaded(zoo::is.zoo)
[1] TRUE
> exists_unloaded(zoo::is.zootoo)
[1] FALSE

(Just be careful, as written exists_unloaded returns TRUE for all strings. Probably want to error if fn is a string.)

edit:

Also, you can call a function without loading the package. I don't know your full use case, but it might obviate the need to check for its existence. (Of course, if the user hasn't installed the package, it will still fail.)

> exists("zoo::is.zoo")
[1] FALSE
> zoo::is.zoo(1)
> z <- zoo::as.zoo(1)
> zoo::is.zoo(z)
[1] TRUE
Appalling answered 28/7, 2015 at 11:33 Comment(2)
parseNamespaceFile('zoo', .libPaths(),mustExist = TRUE) may be useful hereRolan
This answer is wrong. If you call zoo::is.zoo(), that loads the package, as you can see with isLoadedNamespace("zoo"). It doesn't attach the package to the search path. But you can have problems with unloading it later - see detach.Alsworth
R
1

If you didn't want to clutter the search path using loadNamespace would work in conjunction with getAnywhere

Note that this will find functions unexported or exported...

loadNamespace('zoo')
x <- getAnywhere('is.zoo')
x[['where']]=='namespace:zoo'
# TRUE

Wrap it in a function

exist_pkg <- function(f, pkg){
  loadNamespace(pkg)
  x <- getAnywhere(f)
  paste0('namespace:',pkg) %in% x[['where']]
}

you could be careful unloading namespaces afterwards if you really wanted

You could also use getFromNamespace

is.function(getFromNamespace("is.zoo", "zoo"))
# TRUE
Rolan answered 28/7, 2015 at 11:36 Comment(0)
P
1

You can use the exists function to look inside namespaces:

exists2 <- function(x) {

    assertthat::assert_that(assertthat::is.string(x))

    split <- base::strsplit(x, "::")[[1]]

    if (length(split) == 1) {
        base::exists(split[1])
    } else if (length(split) == 2) {
        base::exists(split[2], envir = base::asNamespace(split[1]))
    } else {
        stop(paste0("exists2 cannot handle ", x))
    }
}
Piggin answered 28/7, 2015 at 12:0 Comment(0)
A
1

OK, this needs a more accurate answer.

Short version: you can't.

To see why, consider the following code in a package:

eval(parse(text = paste0("foo <- func", "tion () 1 + 1")))

This will create a function foo. But you will only learn that by running the R code.

You could check the NAMESPACE file for export(foo), but unfortunately the author might have written something like exportPattern("f.*"), so that won't be reliable either.

Long version: you can't avoid loading the package, but you can avoid attaching it. In other words, R will interpret the package source files (and load any dlls), and will store the package in memory, but it won't be directly available on the search path.

ns <- loadNamespace(package)
exists("foo", ns)

You can then unload the namespace with unloadNamespace(package). But see the warnings in ?detach: this is not always guaranteed to work! loadNamespace(package, partial = TRUE) may be helpful, or maybe devtools::load_all and devtools::unload do something cleverer, I don't know.

Some answers are suggesting stuff like try{package::foo}. The problem is that this itself loads the namespace:

> isNamespaceLoaded("broom")
[1] FALSE
> try(broom::tidy)
function(x, ...) UseMethod("tidy")
<environment: namespace:broom>
> isNamespaceLoaded("broom")
[1] TRUE
Alsworth answered 28/2, 2018 at 11:35 Comment(0)
M
0

It isn't a pretty answer, and it probably has some flaws to it, but it's a start.

is_exported <- function(fn, pkg){
  nmsp <- readLines(system.file("NAMESPACE", package = pkg))
  nmsp <- paste0(nmsp, collapse = " ")

  Exports <- stringr::str_extract_all(nmsp,
                                   stringr::regex("(?<=export[(]).+?(?=[)])"))
  Methods <- stringr::str_extract_all(nmsp,
                                   stringr::regex("(?<=S3method[(]).+?(?=[)])"))

  any(grepl(stringr::regex(fn), c(Exports, Methods)))
}

is_exported("is.zoo", "zoo")
Monochord answered 28/7, 2015 at 11:24 Comment(1)
This is roughly what devtools::parse_ns_file does. The problem is that some NAMESPACE files use exportPattern and therefore don't mention exported objects by name... which means this approach has intermittent bugs.Alsworth
I
0

After many tries, the only way I could find that is reasonably fast (~10ms execution time) AND does not clutter the search path was using withr::with_package():

is_exported = function(fun, pkg){
  l = withr::with_package(pkg, try(ls(paste0("package:",pkg))))
  !inherits(l, "try-error")
}

pkg <- "zoo"
fun <- "index"
pkg %in% loadedNamespaces()
#> [1] FALSE
is_exported(fun, pkg)
#> Warning: package 'zoo' was built under R version 4.3.2
#> [1] TRUE
pkg %in% loadedNamespaces()
#> [1] TRUE

Created on 2024-06-26 with reprex v2.1.0

Unloading namespaces after loading them using other techniques described in this thread was 5-10 times slower.

Inclinatory answered 26/6 at 19:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.