# install.packages('pak')
::pak('mjfrigaard/shinypak') pak
6 Dependencies
The previous chapter showed how to document the functions with roxygen2
. In this chapter we’ll cover how to manage dependencies in your new app-package.
Dependencies are the must-have components for your app-package, and they can be divided into imports and exports. Imports are the functions we’re borrowing from add-on/third party packages (i.e., any packages not automatically loaded in a new R session). Exports are the functions, data, and other R objects our app-package offers to users.
Dependencies are handled with in the NAMESPACE
directives (generated via roxygen2
tags) and three fields in the DESCRIPTION
file (Suggests
, Imports
, or Depends
).
Multiple packages can help you manage the dependencies in your app-package, and these are covered in the Dependency hell chapter. This chapter will pick up where we left off with the 05_roxygen2
branch of sap
.
Below is a folder tree of it’s contents:
sap/
├── DESCRIPTION
├── NAMESPACE
├── R/
│ ├── mod_scatter_display.R
│ ├── mod_var_input.R
│ ├── launch_app.R
│ ├── movies_server.R
│ ├── movies_ui.R
│ └── utils.R
├── README.md
├── app.R
├── man/
│ ├── mod_scatter_display_server.Rd
│ ├── mod_scatter_display_ui.Rd
│ ├── mod_var_input_server.Rd
│ ├── mod_var_input_ui.Rd
│ ├── launch_app.Rd
│ ├── movies_server.Rd
│ ├── movies_ui.Rd
│ └── scatter_plot.Rd
├── movies.RData
├── sap.Rproj
└── www/
└── shiny.png
4 directories, 21 files
- 1
-
The
man
folder now contains the help (.Rd
) files for the functions inR/
When in doubt…load, document, and install
During development, you might lose track of the last devtools
function you called (I know I do). If this happens, I’ve found loading, documenting, and installing helps to re-orient me to the current state of the package.
Ctrl/Cmd + Shift + L
ℹ Loading sap
Ctrl/Cmd + Shift + D
==> devtools::document(roclets = c('rd', 'collate', 'namespace'))
ℹ Updating sap documentation
ℹ Loading sap Documentation completed
Ctrl/Cmd + Shift + B
==> R CMD INSTALL --preclean --no-multiarch --with-keep.source sap
* installing to library ‘/path/to/local/install/sap-090c61fc/R-4.2/x86_64-apple-darwin17.0’
* installing *source* package ‘sap’ ...
** using staged installation
** R
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** 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 (sap)
Restarting R session...
> library(sap)
It’s also satisfying to see all three functions execute without any errors!
Identifying dependencies
The first step in managing dependencies is identifying which add-on packages sap
relies on.1 Our goal is to limit the dependencies to only those critical to the functioning of our app, because each additional dependency is a potential point of failure (should this package become unavailable or significantly change).
In the last chapter, we moved and documented the standalone app function (launch_app()
) in the R/
folder. The app.R
file now only contains the following:
# pkgs <- c("shiny", "shinythemes", "stringr", "ggplot2", "rlang")
# install.packages(pkgs, quiet = TRUE)
# packages ------------------------------------
library(shiny)
library(shinythemes)
library(stringr)
library(ggplot2)
library(rlang)
# launch_app ------------------------------------
launch_app()
Ideally, we’ll want to replace these calls to libary()
, but first we have to make sure the functions we’re using in these packages will be available in sap
.
When we run the contents of app.R
, we see the following:
> launch_app() Error in launch_app() : could not find function "launch_app"
Why can’t R find the "launch_app"
function in app.R
?
Let’s recap what we’ve done so far:
app.R
- The
app.R
file loads the necessary packages and callslaunch_app()
:
sap/ └── app.R
R/
- The
R/launch_app.R
file contains the code androxygen2
documentation forlaunch_app()
function:
sap/
└── R/ └── launch_app.R
man/
roxygen2
generates theman/launch_app.Rd
documentation file:
sap/
└── man/ └── launch_app.Rd
6.1 Package exports
The error above is telling us that despite having documentation for launch_app()
in the R/
folder and generating the corresponding .Rd
file in man/
, launch_app()
isn’t being exported from sap
.
The exact cause of the error above becomes more apparent when we try to explicitly namespace launch_app()
from sap
:
::launch_app() sap
Error: 'launch_app' is not an exported object from 'namespace:sap'
Launch app with the shinypak
package:
launch('06.1_pkg-exports')
To make the launch_app()
function available to users of our package, we need to export it. We export functions by including the @export
tag in the roxygen2
comment block (above the function we want to export):
@export
: The function name (my_func
) is not required.#' @export my_func #' my_func <- function() { #' #' }
- Read more here
Export launch_app()
Let’s start by exporting the launch_app()
function from sap
by placing the @export
tag above the function in R/launch_app.R
:
#' Movies app standalone function
#'
#' Wrapper function for `shiny::shinyApp()`
#'
#' @return Shiny app
#'
#'
#' @seealso [mod_var_input_ui()], [mod_var_input_server()], [mod_scatter_display_ui()], [mod_scatter_display_server()]
#'
#' @export
#'
<- function() {
launch_app ::shinyApp(ui = movies_ui, server = movies_server)
shiny }
In app.R
, we’ll replace the calls to library()
with a single call to library(sap)
# packages ------------------------------------
library(sap)
# launch_app ------------------------------------
launch_app()
We’ll load, document, and install the package to generate the NAMESPACE
changes:
Ctrl/Cmd + Shift + L / D / B
Now, when we run the code app.R
, we see the following:
We’ve lost the Shiny icon (www/shiny.png
) in the UI, but we’ll address this in Chapter 9.
launch_app()
launches our application!
The NAMESPACE
file now contains a single export (launch_app
), and when we enter sap::
in the Console, we see the launch_app()
function help file in the tab completion.
What @export
does
We’ll pause here to notice a few things about what @export
does. After loading and documenting our package, the NAMESPACE
is updated with export(launch_app)
, and the Console automatically calls library(sap)
.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Inconsolata', 'primaryColor': '#89D6FB', 'edgeLabelBackground':'#02577A'}}}%% flowchart TD Tag("Tag function with <code>@export</code>") LoadAll("Run <code>load_all()</code>") Document("Run <code>document()</code>") Exported("Function exported to <code>NAMESPACE</code>") NAMESPACE("<code>NAMESPACE</code> updated with <code>export(launch_app)</code>") RdFile("<code>.Rd</code> file created") Tag --> LoadAll Tag --> Document LoadAll --> Exported Document --> NAMESPACE Document --> RdFile Exported --> NAMESPACE style Tag fill:#8dd38d,stroke:none,rx:10,ry:10; style LoadAll fill:#89D6FB,stroke:none,rx:10,ry:10; style Document fill:#89D6FB,stroke:none,rx:10,ry:10; style Exported fill:#FFB64C,stroke:none,rx:10,ry:10; style NAMESPACE fill:#FFB64C,stroke:none,rx:10,ry:10; style RdFile fill:#FFB64C,stroke:none,rx:10,ry:10;
@export
tag makes a function available to anyone using our packageWe can confirm launch_app()
has been exported with ls()
, which returns “the names of the objects in the specified environment.
ls(name = "package:sap")
[1] "launch_app"
The search()
list
library(sap)
attaches sap
to the search list. We can view all the attached packages in the string returned from search()
:
"package:sap" %in% search()
[1] TRUE
What about the add-on/third-party package functions launch_app()
relies on, like ggplot2
? Let’s check to see if ggplot2
is also attached to the search()
list:
c("package:ggplot2") %in% search()
[1] FALSE
Why does this matter? Because if these packages aren’t attached to the search()
list, we can’t call their functions directly (the way we would if we’d loaded the package with library()
). For example, if we try to use ggplot2
to build a plot (similar to the one we have in the app), we see the following:
ggplot(data = mtcars,
aes(x = disp, y = mpg)) +
geom_point()
# Error in ggplot(data = mtcars, aes(x = disp, y = mpg)) :
# could not find function "ggplot"
We can use the add-on/third-party package functions sap
relies on, but we need to explicitly namespace these functions from their original package namespaces (i.e., using pkg::fun()
):
We can use ggplot2
if we explicitly namespace it’s functions
Access to add-on/third-party package functions has implications for the other functions in sap
–for example, the scatter_plot()
function uses ggplot2
functions. But we’re not exporting scatter_plot()
, so when we attempt to run the examples, we see the following error:
scatter_plot()
function without exporting
Examples for ‘sap::scatter_plot’
The message at the top of the Help pane is informative because it tells us that despite scatter_plot()
being functional when we run launch_app()
, it’s not part of the package namespace (and thus, not accessible to users in the help file).
Export scatter_plot()
Let’s add the @export
tag to R/scatter_plot.R
so it’s exported from sap
.
#' Create scatter plot
#'
#' Custom [`ggplot2`](https://ggplot2.tidyverse.org/) function for building scatter plots in `sap`.
#'
#'
#' @param df `data.frame` or `tibble`
#' @param x_var string variable mapped to `x` axis
#' @param y_var string variable mapped to `y` axis
#' @param col_var string variable mapped to `color`
#' @param alpha_var number for point `alpha`
#' @param size_var number for point `size`
#'
#' @return A `ggplot2` plot object
#'
#'
#' @examples
#' scatter_plot(mtcars,
#' x_var = "mpg",
#' y_var = "disp",
#' col_var = "cyl",
#' alpha_var = 0.5,
#' size_var = 3)
#'
#' @seealso [mod_scatter_display_server()]
#'
#' @export
#'
After loading, documenting, and installing sap
, the NAMESPACE
is updated with the export()
directive:
Ctrl/Cmd + Shift + L / D / B
The contents of the updated NAMESPACE
file and typing sap::
in the Console now displays the scatter_plot()
help file in the tab completion:
Below, we confirm users can access the help file for scatter_plot()
and run the examples:
loadedNamespaces()
We’ve already confirmed that ggplot2
isn’t attached with sap
(and hence, it is not included in the search()
list)
c("package:ggplot2") %in% search()
[1] FALSE
However, we can access the functions we used the pkg::fun()
syntax with because those functions are included in the loaded namespaces (which we can view with loadedNamespaces()
)
c("ggplot2") %in% loadedNamespaces()
[1] TRUE
What to @export
‘Always err on the side of caution, and simplicity. It’s easier to give people more functionality than it is to take away stuff they’re used to’ - What to export, R Packages, 2ed
When determining which functions to export, consider the question: “When a user installs and loads sap
, what functions do I want to be available?”
In app-packages, I’ll take the following general approach:
Start by exporting the standalone app function (
launch_app()
)Then selectively export modules and/or functions that perform distinct tasks with potentially reusable functionality (i.e., generate specific UI components, perform data processing tasks, etc.).
I rarely want to export everything from an R package, but it could be helpful if the primary audience for your app-package is other developers within your organization. See the Low-key @export
s with @keywords internal
box below for exporting functions without including them in your package index.
6.2 Package imports
Launch app with the shinypak
package:
launch('06.2_pkg-imports')
Managing imports is slightly more involved than exports because imported dependencies can live in DESCRIPTION
and the NAMESPACE
:
The
DESCRIPTION
file handles package-level dependencies, specifying which add-on packages our app-package uses.The
NAMESPACE
manages function-level access, importing functions from add-on packages to be used in our app-package, and–as we’ve seen above–exporting functions from our app-package for others to use.
Package-level depencencies
The DESCRIPTION
file manages dependencies with three fields: Depends
, Imports
, and Suggests
. 2
Depends
Packages listed under Depends
are essential for our app-package to work. These packages will be attached before our package when library(sap)
is called.
Imports
Packages listed under Imports
are necessary for our app-package to work. These packages are loaded (but not attached) when our app-package is installed.
- Most add-on packages belong under the
Imports
field (i.e., functions from these packages are used in the code belowR/
).
Suggests
The Suggests
field should include any packages that enhance our app-package, but aren’t necessary for the basic functionality. This might include packages used in examples, vignettes, tests, etc.
Function-level access
Function-level access is managed using namespace-qualified references (or ‘explicit namespacing’) in the code below R/
. The NAMESPACE
can also be used to include add-on packages or functions with the @import
and @importFrom
tags.3
pkg::fun()
Refer to add-on package functions using pkg::fun()
syntax in the code below R/
.
@importFrom
@importFrom
should be used when 1) “You can’t call an operator from another package via ::
” 2) “importing a function makes your code much more readable” (not easier to write)
@import
@import
should be used if “you make such heavy use of so many functions from another package that you want to import its entire namespace”
Handling imports
The workflow I use to manage add-on dependencies comes from the advice in the roxygen2
documentation.4
Include the add-on package to the
Imports
field withusethis::use_package()
Refer to add-on functions using explicit namespacing (i.e.,
pkg::fun()
) in the code beneathR/
.
We have some special considerations for the imported add-on functions in our app-package:
Using @import
A substantial portion of the code in sap
comes from shiny
, so we’ll remove the explicit namespacing and place the @import
tag in R/launch_app.R
5
show/hide R/launch_app.R
#' Standalone function
#'
#' Wrapper function for `shinyApp()`
#'
#' @return Shiny app
#'
#'
#' @seealso [mod_var_input_ui()], [mod_var_input_server()],
#' [mod_scatter_display_ui()], [mod_scatter_display_server()]
#'
#' @import shiny
#'
#' @export
#'
- 1
- Import entire shiny package namespace
Using @importFrom
.data
can’t be exported using ::
, so we’ll include @importFrom
in R/scatter_plot.R
. On the other hand, ggplot2
has over 400 functions, so we’ll add the package to the Imports
field and use the namespace-qualified references.6
show/hide R/scatter_plot.R
#' Create scatter plot
#'
#' Custom [`ggplot2`](https://ggplot2.tidyverse.org/) function for building
#' scatter plots in `sap`.
#'
#'
#' @param df `data.frame` or `tibble`
#' @param x_var string variable mapped to `x` axis
#' @param y_var string variable mapped to `y` axis
#' @param col_var string variable mapped to `color`
#' @param alpha_var number for point `alpha`
#' @param size_var number for point `size`
#'
#' @return A `ggplot2` plot object
#'
#'
#' @examples
#' scatter_plot(mtcars,
#' x_var = "mpg",
#' y_var = "disp",
#' col_var = "cyl",
#' alpha_var = 0.5,
#' size_var = 3)
#'
#' @seealso [mod_scatter_display_server()]
#'
#' @importFrom rlang .data
#'
#' @export
#'
- 1
-
Import a the
.data
operator fromrlang
use_package('pkg')
As an example, we’ll add the bslib
package and update our app UI layout:
::use_package('bslib') usethis
✔ Setting active project to '/Users/mjfrigaard/projects/apps/sap'
✔ Adding 'bslib' to Imports field in DESCRIPTION
• Refer to functions with `bslib::fun()`
In movies_ui()
, we’ll change the fluidPage()
to the bslib::page_fillable()
and adjust move the data source information to the bslib::card_footer()
:
updated movies_ui() bslib function
<- function() {
movies_ui tagList(
::page_fillable(
bslibh1("Movie Reviews"),
::layout_sidebar(
bslibsidebar =
::sidebar(
bslibtitle = tags$h4("Sidebar inputs"),
img(
src = "shiny.png",
height = 60,
width = 55,
style = "margin:10px 10px"
),mod_var_input_ui("vars")
),::card(
bslibfull_screen = TRUE,
::card_header(
bslib$h4("Scatter Plot")
tags
),mod_scatter_display_ui("plot"),
::card_footer(
bslib$blockquote(
tags$em(
tags$p(
tags"The data for this application comes from the ",
$a("Building web applications with Shiny",
tagshref = "https://rstudio-education.github.io/shiny-course/"
),"tutorial"
)
)
)
)
)
)
)
) }
After adding all add-on packages to the DESCRIPTION
with usethis::use_package()
, then deciding if/where to use @importFrom
and @import
, we’ll load, document, and install sap
:
Ctrl/Cmd + Shift + L / D / B
When we review the updated NAMESPACE
and DESCRIPTION
files, we should see the following:
Updated NAMESPACE
with @import
and @importFrom
# Generated by roxygen2: do not edit by hand
export(launch_app)
export(scatter_plot)
import(shiny)
importFrom(rlang,.data)
Updated DESCRIPTION
with all Imports
:
Package: sap
Title: Shiny App-Packages
Version: 0.0.0.9000
Author: John Smith <John.Smith@email.io> [aut, cre]
Maintainer: John Smith <John.Smith@email.io>
Description: An R package with a collection of Shiny applications.
License: GPL-3
Imports:
bslib,
ggplot2,
rlang,
shiny,
shinythemes,
stringr,
tools
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
roxygen2
will update the NAMESPACE
, but usethis::use_package()
is needed to update the DESCRIPTION
.
When we run launch_app()
, we see the application launches and we can still run the scatter_plot()
examples:
What @import
does
The figure below attempts to capture some of confusion between the dependencies listed in the NAMESPACE
and the Imports
field in the DESCRIPTION
file.7
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Inconsolata'}}}%% flowchart TD Imports("List package under <code>Imports</code> in <code>DESCRIPTION</code>") Reference("Reference as <code>pkg::fun()</code> in code") LoadAll("Run <code>load_all()</code>") Document("Run <code>document()</code>") NamespaceUpdated("<code>NAMESPACE</code> updated") RdFileUpdated("<code>.Rd</code> file updated") Imports --> Reference Reference --> LoadAll Reference --> Document LoadAll --> NamespaceUpdated Document --> RdFileUpdated NamespaceUpdated --> RdFileUpdated style Imports fill:#8dd38d,stroke:none,rx:10,ry:10; style Reference fill:#8dd38d,stroke:none,rx:10,ry:10; style LoadAll fill:#89D6FB,stroke:none,rx:10,ry:10; style Document fill:#89D6FB,stroke:none,rx:10,ry:10; style NamespaceUpdated fill:#FFB64C,stroke:none,rx:10,ry:10; style RdFileUpdated fill:#FFB64C,stroke:none,rx:10,ry:10;
devtools::document
does not change the DESCRIPTION
filedevtools::document()
(or Ctrl/Cmd + Shift + D) updates the NAMESPACE
with any @import
, @importFrom
or @export
tags. However, no changes are made to the DESCRIPTION
file.
Let’s confirm we’re still only exporting launch_app()
and scatter_plot()
from sap
:
ls(name = "package:sap")
[1] "launch_app" "scatter_plot"
Great. Now we’ve listed five packages in the Imports
field of the DESCRIPTION
file:
Imports:
ggplot2,
rlang,
shiny,
shinythemes, stringr
The search()
list
Are these packages on the search list?
<- c("package:ggplot2", "package:rlang",
pkgs "package:shiny", "package:shinythemes",
"package:stringr")
%in% search() pkgs
[1] FALSE FALSE FALSE FALSE FALSE
This demonstrates that none of these packages are attached with sap
.
loadedNamespaces()
However, the rlang
and shiny
packages are included in the loadedNamespaces()
(because we included them with @import
/@importFrom
).
<- c("ggplot2", "rlang", "shiny",
pkgs "shinythemes", "stringr")
%in% loadedNamespaces() pkgs
[1] FALSE TRUE TRUE FALSE FALSE
We can still access the add-on package functions in sap
using the pkg::fun()
syntax:
More information on imports
Below are handful of questions and answers I’ve encountered regarding package imports:
Question: How can I include an add-on package to my DESCRIPTION
file?
Answer: usethis::use_package()
automatically adds a package in the Imports
section, and has options for specifying the minimum version.
Question: Will users of my app-package have access to the packages listed in the Imports
field of my DESCRIPTION
file?
Answer: library(pkg)
loads the namespace of imported packages, but they are not attached to the search()
path.8
Question: How can I tell the difference between functions written by a package author and imported functions in the code below R/
?
Answer: using pkg::fun()
makes calls to add-on packages explicit and easy to differentiate from the native functions developed in sap
.9
Question: What does the NAMESPACE
do when my package is installed by a user?
Answer: managing the NAMESPACE
ensures your app-package works when it’s installed and loaded on another machine, because R will read your package namespace to find what it imports and exports.10
Question: Where should I place the @importFrom
tag in the code below R/?
Answer: place the @importFrom pkg fun
tag directly above the code using the add-on function. You can also consolidate all @import
and @importFrom
tags into a single package doc file (i.e., R/[sap]-package.R
) by calling usethis::use_package_doc()
.
Question: Should I be using @importFrom
or @import
from?
Answer: prefer @importFrom
over @import
, but try to avoid using either11 12.
Question: Where can I find more information about package namespaces and imports?
Answer: Imports are described briefly in R Packages, 2ed13 and covered in-depth in Advanced R, 2ed.14
- “Each namespace has an imports environment that can contain bindings to functions used by the package that are defined in another package.”
- “The imports environment is controlled by the package developer with the
NAMESPACE
file. Specifically, directives such asimportFrom()
andimports()
populate this environment.”
In order for app-package to work, users needs to have access to any add-on packages that are called in the code below R/
. Knowing when, why, how and what happens to imports helps you decide how to fit these habits into your package development workflow.
Checking dependencies
With all the moving parts in dependency management, it can be easy to forget if you’ve documented everything correctly. So far we haven’t covered using devtools::check()
as part of your app-package habits (which is fine), but this is one area it’s particularly helpful.
For example, if I had listed shiny
as an import using the @import
tag (resulting in the import(shiny)
directive in the NAMESPACE
), devtools::check()
would produce the following error:
── R CMD check results ────────────────────────── sap 0.0.0.9000 ────
Duration: 7.4s
❯ checking package dependencies ... ERROR
Namespace dependency missing from DESCRIPTION Imports/Depends entries: ‘shiny’
See section ‘The DESCRIPTION file’ in the ‘Writing R Extensions’
manual.
1 error ✖ | 0 warnings ✔ | 0 notes ✔
Error: R CMD check found ERRORs
Execution halted
Exited with status 1.
Recap
Below are the main takeaways from managing the imports and exports from your app-package:
If you’d like to read more about package dependencies, I recommend Writing R Extensions (specifically the sections on dependencies 15 and namespaces 16).
In the next section, we’ll cover how to ensure the movies.RData
can be stored and loaded in our app-package!
I’ve made this process somewhat easier by explicitly namespacing all of the add-on package functions in
sap
(i.e., withpkg::fun()
).↩︎Additional fields exists (i.e.,
Remotes
), but these are special circumstances.↩︎Read more about this in the section titled, ‘In code below R/’ in R Packages, 2ed↩︎
“if you are using just a few functions from another package, we recommending adding the package to the
Imports:
field of theDESCRIPTION
file and calling the functions explicitly using::
, e.g.,pkg::fun()
”…“If the repetition of the package name becomes annoying you can@importFrom
and drop thepkg::fun()
”. Read more in the Importing functions section on the package website:↩︎Using
@import
is not generally considered best practice, but it makes sense for app-packages: …for Shiny apps, I recommend using@import shiny
to make all the functions in the Shiny package easily available. Mastering Shiny, R CMD check”↩︎Read more about using
ggplot2
in packages in the section titled, ‘Referring toggplot2
functions’↩︎See the section titled, ‘Confusion about Imports’ in R Packages, 2ed, “Listing a package in
Imports
inDESCRIPTION
does not ‘import’ that package.”↩︎Users can access functions from add-on packages with the
pkg::fun
syntax.↩︎“Our recommended default is to call external functions using the
package::function()
syntax.” - R Packages, 2ed↩︎The namespace controls the search strategy for variables used by functions in the package. If not found locally, R searches the package namespace first, then the imports, then the base namespace and then the normal search path (so the base namespace precedes the normal search rather than being at the end of it). - Writing R Extensions↩︎
“Using
importFrom
selectively rather thanImports
is good practice and recommended notably when importing from packages with more than a dozen exports and especially from those written by others (so what they export can change in future).” - Specifying imports and exports.↩︎“Specifically, we recommend that you default to not importing anything from [add-on packages] into your namespace. This makes it very easy to identify which functions live outside of your package, which is especially useful when you read your code in the future. This also eliminates any concerns about name conflicts between [add-on packages] and your package.” - R Packages, 2ed↩︎
See the Function lookup inside a package section of R Packages, 2ed↩︎
See the Package environments and the search path of Advanced R, 2ed↩︎
See section 1.1.3 Package Dependencies in Writing R Extensions↩︎
See section 1.5, Package namespaces in Writing R Extensions↩︎