rticulate/import

avoiding the use of `library` in Shiny apps

Closed this issue ยท 10 comments

vnijs commented

Shiny apps require the use of library or require. It seems that the import package would be a good option when an app requires several packages. My shiny app is an R-package with a namespace. I have been trying to figure out how to import the various functions from different packages (see attempt below) but haven't gotten things working yet. Any ideas / suggestions? Related to #4

copy_imported <- function(.from) {

  from <- as.character(substitute(.from))

  import_list <- getNamespaceImports(from)
  parent  <- parent.frame()
  import_names <- names(import_list)

  for (i in unique(import_names)) {
    if (i %in% c("base","shiny","magrittr")) next

    symbols <- unlist(import_list[which(i == import_names)])

    for (j in symbols) {
      # do.call(import::from, list(i = as.symbol(i), j = as.symbol(j)))
      fn <- get(j, envir = asNamespace(i), inherits = TRUE)
      assign(j, eval.parent(call("function", formals(fn), body(fn))), parent)
    }
  }

  invisible(NULL)
}

To be honest, I am not sure exatly what it is you want. Can you provide a minimum working example packge where a problem is obvious?

vnijs commented

In most shiny apps you simply specify a number of libraries to load. For example, if r_pkgs is a list of package names you could use something like the below. I'd rather not do that and use import instead. However, the list of functions to import can be pretty long and, if the shiny app is part of a package, is likely already specified in NAMESPACE. I tried to write a function that could be used to (1) query the package namespace and (2) import all functions specified there. Does this sound feasible?

sapply(r_pkgs, require, character.only = TRUE)

If you are deploying a shiny app as a package and want to avoid using library or require statements in server.r itself, one simple option is use depends rather than imports in the package description. I agree that adding support for shiny apps to be evaluated locally in a package (and therefore being able to use functions imported using the namespace) would be great.

Thinking ...

  1. Why is library not good enough here, if you're aiming at importing everything anyways? Is it because private objects are not exposed?

  2. I think it would be better if shiny::runApp would allow running within the current environment, if say .packageName exists; specifically to allow packages to bundle apps, which could make use of both public and private API of the package.

@vnijs
Here's a simple example of how I'd do it, if I understand your problem correctly.
appex.zip

vnijs commented

@smbache That is a really interesting approach! I don't think it would work, however, when running on shiny-server. Suppose a shiny-app is not installed as a package on a server but available only as source code in a directory (i.e., the usual approach for shiny apps). The namespace file does, however, have the functions needed in the app for when it is run locally. So rather than use library(psych), for example, it would be preferable to source only the functions needed, thereby minimizing the risk of conflicting function names etc..

vnijs commented

@smbache For now I'm use the following in global.R to import specific functions when (1) the shiny app is in an R-package and (2) is run on a server. Seems to work ok ... although I'd prefer to avoid eval-parse. Perhaps import could have a character.only option like library?

import_fs <- function(ns, libs = c(), incl = c(), excl = c()) {
  tmp <- sapply(libs, library, character.only = TRUE); rm(tmp)
  if (length(incl) != 0 || length(excl) != 0) {
    import_list <- getNamespaceImports(ns)
    if (length(incl) == 0)
      import_list[names(import_list) %in% c("base", "methods", "stats", "utils", libs, excl)] <- NULL
    else
      import_list <- import_list[names(import_list) %in% incl]
    import_names <- names(import_list)

    for (i in seq_len(length(import_list))) {
      fun <- import_list[[i]]
      lib <- import_names[[i]]
      eval(parse(text = paste0("import::from(",lib,", '",paste0(fun,collapse="', '"),"')")))
    }
  }
  invisible()
}

if (!"package:myrpackage" %in% search())
  import_fs("myrpackage", incl = c("magrittr","ggplot2","lubridate","tidyr","dplyr","broom"))

Sounds good to me @vnijs : I just pushed a version with .character_only:

funs <- c("select", "mutate", "keep_when" = "filter")
import::from("dplyr", funs, .character_only = TRUE)
vnijs commented

Thank you @smbache.