rnamespaces

How to import an R package from within a function?


I am interested in how to write a function that can be used for an arbitrary number o R packages to

  1. Install package if necessary
  2. Make package functions available from global namespace

I tried multiple variations of the following:

import <- function(...) {
  #' Import R packages. Install them if necessary.
  #' 
  #' @param ... any argument that can be passed to install.packages. Provide
  #' package names as character strings.
  #' @details The function installs only packages that are missing. Packages
  #' are loaded.
  #' @examples
  #' # Load packages
  #' import("dplyr", "MASS", "terra", dependencies = TRUE)
  #' 
  #' @seealso \code{\link[base]{install.packages}}
  #' @export
  args <- list(...)
  packages = args[names(args) == ""]
  kwargs = args[names(args) != ""]
  
  for (package in packages) {
    if (!requireNamespace(package, quietly = TRUE)) {
      do.call(install.packages, c(list(package), kwargs))
    }
    #base::library(package, character.only = TRUE, pos = .GlobalEnv)
    eval(
      bquote(library(.(package), character.only = TRUE)),
      envir = .GlobalEnv
      )
  }
}

With this function, when I run

import(
  "lubridate", "tidyverse", "ncdf4", "viridis", "terra", "rworldmap", "EFDR",
  "RColorBrewer"
  )

I cannot use functions directly (e.g., nc_close()), but I need to specify the package (ncdf4::nc_close()). Is there a way to make the functions directly available (without running require on the packages again, outside the import function)?

Edit

The problem was not that the package import was done within a function, but rather, the issue was that that function was not robust in handling args and kwargs. Here is a working version:

import <- function(...) {
  #' Import R packages. Install them if necessary.
  #' 
  #' @param ... any argument that can be passed to install.packages. Provide
  #' package names as character strings.
  #' @details The function installs only packages that are missing. Packages
  #' are loaded.
  #' @examples
  #' # Load packages
  #' import("dplyr", "MASS", "terra", dependencies = TRUE)
  #' 
  #' @seealso \code{\link[base]{install.packages}}
  #' @export
  args <- list(...)
  arg_names <- names(args)
  
  packages = if (is.null(arg_names)) {
    args
  } else {
    args[arg_names == "" | is.null(arg_names)]
  }
  
  kwargs = if (is.null(arg_names)) {
    list()
  } else {
    args[!is.null(arg_names) & arg_names != ""]
  }
  
  load <- function(package) {
    if (!requireNamespace(package, quietly = TRUE)) {
      do.call(install.packages, c(list(package), kwargs))
    }
    base::library(package, character.only = TRUE, pos = .GlobalEnv)
  }
  invisible(lapply(packages, load))
}

import(
  "lubridate", "tidyverse", "ncdf4", "viridis", "terra", "rworldmap", "EFDR",
  "RColorBrewer"
  )

Solution

  • I think this does the trick:

    import <- function(x) {
      if (!require(x, character.only = TRUE, quiet = TRUE)) install.packages(x)
      require(x, character.only = TRUE)
    }
    

    Example:

    > geom_ridgeline()
    Error in geom_ridgeline() : could not find function "geom_ridgeline"
    
    > import('ggridges')
    Installing package into ‘/home/zeloff/R/4.4’
    (as ‘lib’ is unspecified)
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100 2179k  100 2179k    0     0  1690k      0  0:00:01  0:00:01 --:--:-- 1691k
    * installing *source* package ‘ggridges’ ...
    ** package ‘ggridges’ successfully unpacked and MD5 sums checked
    ** using staged installation
    ** R
    ** data
    *** moving datasets to lazyload DB
    ** inst
    ** byte-compile and prepare package for lazy loading
    ** help
    *** installing help indices
    *** copying figures
    ** building package indices
    ** installing vignettes
    ** testing if installed package can be loaded from temporary location
    ** testing if installed package can be loaded from final location
    ** testing if installed package keeps a record of temporary installation path
    * DONE (ggridges)
    
    The downloaded source packages are in
            ‘/tmp/RtmpefUHEC/downloaded_packages’
    Loading required package: ggridges
    
    > geom_ridgeline()
    geom_ridgeline: na.rm = FALSE
    stat_identity: na.rm = FALSE
    position_identity
    

    Update

    If you want to import multiple packages at once, just wrap the import function in an lapply:

    import <- function(...) {
      imp <- function(x) {
        if (!require(x, character.only = TRUE, quiet = TRUE))
          install.packages(x)
        require(x, character.only = TRUE)
      }
    
      x <- list(...)
      invisible(lapply(x, imp))
    }