12  Dependencies: In Practice

Second edition

You are reading the work-in-progress second edition of R Packages. This chapter should be readable but is currently undergoing final polishing.

12.1 Introduction

This chapter presents the practical details of working with your dependencies inside your package. If you need a refresher on any of the background:

  • Chapter 10 covers the DESCRIPTION file. Listing a dependency in that file, such as in Imports, is a necessary first step when taking a dependency.

  • Section 11.2 provides a decision-making framework for dependencies.

  • The technical details of package namespaces, the search path, and attaching vs. loading are laid out in Section 11.3, Section 11.4, and Section 11.5.

We’re finally ready to talk about how to use different types of dependencies within the different parts of your package:

  • in your functions, below R/
  • in your tests, below tests/testthat/
  • in your examples, in the help topics below man/
  • in your vignettes and articles, below vignettes/

12.2 Confusion about Imports

Let’s make this crystal clear:

Listing a package in Imports in DESCRIPTION does not “import” that package.

It is natural to assume that listing a package in Imports actually “imports” the package, but this is just an unfortunate choice of name for the Imports field. The Imports field makes sure that the packages listed there are installed when your package is installed. It does not make those functions available to you, e.g. below R/, or to your user.

It is neither automatic nor necessarily advisable that a package listed in Imports also appears in NAMESPACE via imports() or importFrom(). It is common for a package to be listed in Imports in DESCRIPTION, but not in NAMESPACE. The converse is not true. Every package mentioned in NAMESPACE must also be present in the Imports or Depends fields.

12.3 Conventions for this chapter

Sometimes our examples can feature real functions from real packages. But if we need to talk about a generic package or function, here are the conventions we use below:

  • pkg: the name of your hypothetical package

  • aaapkg or bbbpkg: the name of a hypothetical package your package depends on

  • aaa_fun(): the name of a function exported by aaapkg

12.4 NAMESPACE Workflow

In the sections below, we give practical instructions on how (and when) to import functions from another package into yours and how to export functions from your package. The file that keeps track of all this is the NAMESPACE file (more details in Section 11.3.2).

In the devtools workflow and this book, we generate the NAMESPACE file from special comments in the R/*.R files. Since the package that ultimately does this work is roxygen2, these are called “roxygen comments”. These roxygen comments are also the basis for your package’s help topics, which is covered in Section 17.2.1.

The NAMESPACE file starts out with a single commented-out line explaining the situation (and hopefully discouraging any manual edits):

# Generated by roxygen2: do not edit by hand

As you incorporate roxygen tags to export and import functions, you need to re-generate the NAMESPACE file periodically. Here is the general workflow for regenerating NAMESPACE (and your documentation):

  1. Add namespace-related tags to the roxygen comments in your R/*.R files. This is an artificial example, but it gives you the basic idea:

    #' @importFrom aaapkg aaa_fun
    #' @import bbbpkg
    #' @export
    foo <- function(x, y, z) {
      ...
    }
  2. Run devtools::document() (or press Ctrl/Cmd + Shift + D in RStudio) to “document” your package. By default, two things happen:

    • The help topics in the man/*.Rd files are updated (covered in Chapter 17).

    • The NAMESPACE file is re-generated. In our example, the NAMESPACE file would look like:

      # Generated by roxygen2: do not edit by hand
      
      export(foo)
      import(bbbpkg)
      importFrom(aaapkg,aaa_fun)

Roxygen2 is quite smart and will insert the appropriate directive in NAMESPACE, i.e. it can usually determine whether to use export() or S3method().

RStudio

Press Ctrl/Cmd + Shift + D, to generate your package’s NAMESPACE (and man/*.Rd files). This is also available via Document in the Build menu and pane.

12.5 Package is listed in Imports

Consider a dependency that is listed in DESCRIPTION in Imports:

Imports:
    aaapkg

The code inside your package can assume that aaapkg is installed whenever pkg is installed.

12.5.1 In code below R/

Our recommended default is to call external functions using the package::function() syntax:

somefunction <- function(...) {
  ...
  x <- aaapkg::aaa_fun(...)
  ...
}

Specifically, we recommend that you default to not importing anything from aaapkg 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 aaapkg and your package.

Of course there are reasons to make exceptions to this rule and to import something from another package into yours:

  • An operator: You can’t call an operator from another package via ::, so you must import it. Example: the null-coalescing operator %||% from rlang or the original pipe %>% from magrittr.

  • A function that you use a lot: If importing a function makes your code much more readable, that’s a good enough reason to import it. This literally reduces the number of characters required to call the external function. This can be especially handy when generating user-facing messages, because it makes it more likely that lines in the source correspond to lines in the output.

  • A function that you call in a tight loop: There is a minor performance penalty associated with ::. It’s on the order of 100ns, so it will only matter if you call the function millions of times.

A handy function for your interactive workflow is usethis::use_import_from():

usethis::use_import_from("glue", "glue_collapse")

The call above writes this roxygen tag into the source code of your package:

#' @importFrom glue glue_collapse

Where should this roxygen tag go? There are two reasonable locations:

  • As close as possible to the usage of the external function. With this mindset, you would place @importFrom in the roxygen comment above the function in your package where you use the external function. If this is your style, you’ll have to do it by hand. We have found that this feels natural at first, but starts to break down as you use more external functions in more places.

  • In a central location. This approach keeps all @importFrom tags together, in a dedicated section of the package-level documentation file (which can be created with usethis::use_package_doc()). This is what use_import_from() implements. So, in R/pkg-package.R, you’ll end up with something like this:

    # The following block is used by usethis to automatically manage
    # roxygen namespace tags. Modify with care!
    ## usethis namespace: start
    #' @importFrom glue glue_collapse
    ## usethis namespace: end
    NULL
    #> NULL

Recall that devtools::document() processes your roxygen comments (Section 12.4), which writes help topics to man/*.Rd and, relevant to our current goal, generates the NAMESPACE file. If you use use_import_from(), it does this for you and also calls load_all(), making the newly imported function available in your current session.

The roxygen tag above cause this directive to appear in the NAMESPACE file:

importFrom(glue, glue_collapse)

Now you can use the imported function directly in your code:

somefunction <- function(...) {
  ...
  x <- glue_collapse(...)
  ...
}

Sometimes you make such heavy use of so many functions from another package that you want to import its entire namespace. This should be relatively rare. In the tidyverse, the package we most commonly treat this way is rlang, which functions almost like a base package for us.

Here is the roxygen tag that imports all of rlang. This should appear somewhere in R/*.R, such as the dedicated space described above for collecting all of your namespace import tags.

#' @import rlang

After calling devtools::document(), this roxygen tag causes this directive to appear in the NAMESPACE file:

import(rlang)

This is the least recommended solution because it can make your code harder to read (you can’t tell where a function is coming from), and if you @import many packages, it increases the chance of function name conflicts. Save this for very special situations.

12.5.1.1 How to not use a package in Imports

Sometimes you have a package listed in Imports, but you don’t actually use it inside your package or, at least, R doesn’t think you use it. That leads to a NOTE from R CMD check:

* checking dependencies in R code ... NOTE
Namespace in Imports field not imported from: ‘aaapkg’
  All declared Imports should be used.

This can happen if your only use of aaapkg is a call like aaapkg::aaa_fun() in the default for a function argument in your package. This also comes up if you want to list an indirect dependency in Imports, so you can state a minimum version for it. The tidyverse meta-package has this problem on a large scale, since it exists mostly to install a bundle of packages at specific versions.

How can you get rid of this NOTE?

Our recommendation is to put a namespace-qualified reference (not a call) to an object in aaapkg in some file below R/, such as a .R file associated with package-wide setup:

ignore_unused_imports <- function() {
  aaapkg::aaa_fun
}

You don’t need to call ignore_unused_imports() anywhere. You shouldn’t export it. You don’t have to actually exercise aaapkg::aaa_fun(). What’s important is to access something in aaapkg’s namespace with ::.

An alternative approach you might be tempted to use is to import aaapkg::aaa_fun() into your package’s namespace, probably with the roxygen tag @importFrom aaapkg aaa_fun. This does suppress the NOTE, but it also does more. This causes aaapkg to be loaded whenever your package is loaded. In contrast, if you use the approach we recommend, the aaapkg will only be loaded if your user does something actually requires it. This rarely matters in practice, but it’s always nice to minimize or delay the loading of additional packages.

12.5.2 In test code

Refer to external functions in your tests just as you refer to them in the code below R/. Usually this means you should use aaapkg::aaa_fun(). But if you have imported a particular function, either specifically or as part of an entire namespace, you can just call it directly in your test code.

It’s generally a bad idea to use library(aaapkg) to attach one of your dependencies somewhere in your tests, because it makes the search path in your tests different from how your package actually works. This is covered in more detail in Section 15.2.5.

12.5.3 In examples and vignettes

If you use a package that appears in Imports in one of your examples or vignettes, you’ll need to either attach the package with library(aaapkg) or use a aaapkg::aaa_fun()-style call. You can assume that aaapkg is available, because that’s what Imports guarantees. Read more in Section 17.6.4 and Section 18.5.

12.6 Package is listed in Suggests

Consider a dependency that is listed in DESCRIPTION in Suggests:

Suggests:
    aaapkg

You can NOT assume that every user has installed aaapkg (but you can assume that a developer has). Whether a user has aaapkg will depend on how they installed your package. Most of the functions that are used to install packages support a dependencies argument that controls whether to install just the hard dependencies or to take a more expansive approach, which includes suggested packages:

install.packages(dependencies =)
remotes::install_github(dependencies =)
pak::pkg_install(dependencies =)

Broadly speaking, the default is to not install packages in Suggests.

12.6.1 In code below R/

Inside a function in your own package, check for the availability of a suggested package with requireNamespace("aaapkg", quietly = TRUE). There are two basic scenarios: the dependency is absolutely required or your package offers some sort of fallback behaviour.

# the suggested package is required 
my_fun <- function(a, b) {
  if (!requireNamespace("aaapkg", quietly = TRUE)) {
    stop(
      "Package \"aaapkg\" must be installed to use this function.",
      call. = FALSE
    )
  }
  # code that includes calls such as aaapkg::aaa_fun()
}

# the suggested package is optional; a fallback method is available
my_fun <- function(a, b) {
  if (requireNamespace("aaapkg", quietly = TRUE)) {
    aaapkg::aaa_fun()
  } else {
    g()
  }
}

The rlang package has some useful functions for checking package availability: rlang::check_installed() and rlang::is_installed(). Here’s how the checks around a suggested package could look if you use rlang:

# the suggested package is required 
my_fun <- function(a, b) {
  rlang::check_installed("aaapkg", reason = "to use `aaa_fun()`")
  # code that includes calls such as aaapkg::aaa_fun()
}

# the suggested package is optional; a fallback method is available
my_fun <- function(a, b) {
  if (rlang::is_installed("aaapkg")) {
    aaapkg::aaa_fun()
  } else {
    g()
  }
}

These rlang functions have handy features for programming, such as vectorization over pkg, classed errors with a data payload, and, for check_installed(), an offer to install the needed package in an interactive session.

12.6.2 In test code

The tidyverse team generally writes tests as if all suggested packages are available. That is, we use them unconditionally in the tests.

The motivation for this posture is self-consistency and pragmatism. The key package needed to run tests is testthat and it appears in Suggests, not in Imports or Depends. Therefore, if the tests are actually executing, that implies that an expansive notion of package dependencies has been applied.

Also, empirically, in every important scenario of running R CMD check, the suggested packages are installed. This is generally true for CRAN and we ensure that it’s true in our own automated checks. However, it’s important to note that other package maintainers take a different stance and choose to protect all usage of suggested packages in their tests and vignettes.

Sometimes even we make an exception and guard the use of a suggested package in a test. Here’s a test from ggplot2, which uses testthat::skip_if_not_installed() to skip execution if the suggested sf package is not available.

test_that("basic plot builds without error", {
  skip_if_not_installed("sf")

  nc_tiny_coords <- matrix(
    c(-81.473, -81.741, -81.67, -81.345, -81.266, -81.24, -81.473,
      36.234, 36.392, 36.59, 36.573, 36.437, 36.365, 36.234),
    ncol = 2
  )

  nc <- sf::st_as_sf(
    data_frame(
      NAME = "ashe",
      geometry = sf::st_sfc(sf::st_polygon(list(nc_tiny_coords)), crs = 4326)
    )
  )

  expect_doppelganger("sf-polygons", ggplot(nc) + geom_sf() + coord_sf())
})

What might justify the use of skip_if_not_installed()? In this case, the sf package can be nontrivial to install and it is conceivable that a contributor would want to run the remaining tests, even if sf is not available.

Finally, note that testthat::skip_if_not_installed(pkg, minimum_version = "x.y.z") can be used to conditionally skip a test based on the version of the other package.

12.6.3 In examples and vignettes

Another common place to use a suggested package is in an example and here we often guard with require() or requireNamespace(). This example is from ggplot2::coord_map(). ggplot2 lists the maps package in Suggests.

#' @examples
#' if (require("maps")) {
#'   nz <- map_data("nz")
#'   # Prepare a map of NZ
#'   nzmap <- ggplot(nz, aes(x = long, y = lat, group = group)) +
#'     geom_polygon(fill = "white", colour = "black")
#'  
#'   # Plot it in cartesian coordinates
#'   nzmap
#' }

An example is basically the only place where we would use require() inside a package. Read more in Section 11.5.

Our stance regarding the use of suggested packages in vignettes is similar to that for tests. The key packages needed to build vignettes (rmarkdown and knitr) are listed in Suggests. Therefore, if the vignettes are being built, it’s reasonable to assume that all of the suggested packages are available. We typically use suggested packages unconditionally inside vignettes.

But if you choose to use suggested packages conditionally in your vignettes, the knitr chunk option eval is very useful for achieving this. See Section 18.5 for more.

12.7 Package is listed in Depends

Consider a dependency that is listed in DESCRIPTION in Depends:

Depends:
    aaapkg

This situation has a lot in common with a package listed in Imports. The code inside your package can assume that aaapkg is installed on the system. The only difference is that aaapkg will be attached whenever your package is.

12.7.1 In code below R/ and in test code

Your options are exactly the same as using functions from a package listed in Imports:

  • Use the aaapkg::aaa_fun() syntax.

  • Import an individual function with the @importFrom aaapkg aaa_fun roxygen tag and call aaa_fun() directly.

  • Import the entire aaapkg namespace with the @import aaapkg roxygen tag and call any function directly.

The main difference between this situation and a dependency listed in Imports is that it’s much more common to import the entire namespace of a package listed in Depends. This often makes sense, due to the special dependency relationship that motivated listing it in Depends in the first place.

12.7.2 In examples and vignettes

This is the most obvious difference with a dependency in Depends versus Imports. Since your package is attached when your examples are executed, so is the package listed in Depends. You don’t have to attach it explicitly with library(aaapkg).

The ggforce package Depends on ggplot2 and the examples for ggforce::geom_mark_rect() use functions like ggplot2::ggplot() and ggplot2::geom_point() without any explicit call to library(ggplot2):

ggplot(iris, aes(Petal.Length, Petal.Width)) +
  geom_mark_rect(aes(fill = Species, filter = Species != 'versicolor')) +
  geom_point()
# example code continues ...

The first line of code executed in one of your vignettes is probably library(pkg), which attaches your package and, as a side effect, attaches any dependency listed in Depends. You do not need to explicitly attach the dependency before using it. The censored package Depends on the survival package and the code in vignette("examples", package = "censored") starts out like so:

library(tidymodels)
library(censored)
#> Loading required package: survival

# vignette code continues ...

12.8 Package is a nonstandard dependency

In packages developed with devtools, you may see DESCRIPTION files that use a couple other nonstandard fields for package dependencies specific to development tasks.

12.8.1 Depending on the development version of a package

The Remotes field can be used when you need to install a dependency from a nonstandard place, i.e. from somewhere besides CRAN or Bioconductor. One common example of this is when you’re developing against a development version of one of your dependencies. During this time, you’ll want to install the dependency from its development repository, which is often GitHub. The way to specify various remote sources is described in a devtools vignette.

The dependency and any minimum version requirement still need to be declared in the normal way in, e.g., Imports. usethis::use_dev_package() helps to make the necessary changes in DESCRIPTION. If your package temporarily relies on a development version of aaapkg, the affected DESCRIPTION fields might evolve like this:

Stable -->               Dev -->                       Stable again
----------------------   ---------------------------   ----------------------
Package: pkg             Package: pkg                  Package: pkg
Version: 1.0.0           Version: 1.0.0.9000           Version: 1.1.0
Imports:                 Imports:                      Imports: 
    aaapkg (>= 2.1.3)       aaapkg (>= 2.1.3.9000)       aaapkg (>= 2.2.0)
                         Remotes:   
                             jane/aaapkg 
CRAN

It’s important to note that you should not submit your package to CRAN in the intermediate state, meaning with a Remotes field and with a dependency required at a version that’s not available from CRAN or Bioconductor. For CRAN packages, this can only be a temporary development state, eventually resolved when the dependency updates on CRAN and you can bump your minimum version accordingly.

12.8.2 Config/Needs/* field

You may also see devtools-developed packages with packages listed in DESCRIPTION fields in the form of Config/Needs/*, which we described in Section 10.9.

The use of Config/Needs/* is not directly related to devtools. It’s more accurate to say that it’s associated with continuous integration workflows made available to the community at https://github.com/r-lib/actions/ and exposed via functions such as usethis::use_github_actions(). A Config/Needs/* field tells the setup-r-dependencies GitHub Action about extra packages that need to be installed.

Config/Needs/website is the most common and it provides a place to specify packages that aren’t a formal dependency, but that must be present in order to build the package’s website (Chapter 20). The readxl package is a good example. It has a non-vignette article on workflows that shows readxl working in concert with other tidyverse packages, such as readr and purrr. But it doesn’t make sense for readxl to have a formal dependency on readr or purrr or (even worse) the tidyverse!

On the left is what readxl has in the Config/Needs/website field of DESCRIPTION to indicate that the tidyverse is needed in order to build the website, which is also formatted with styling that lives in the tidyverse/template GitHub repo. On the right is the corresponding excerpt from the configuration of the workflow that builds and deploys the website.

in DESCRIPTION                  in .github/workflows/pkgdown.yaml
--------------------------      ---------------------------------
Config/Needs/website:           - uses: r-lib/actions/setup-r-dependencies@v2
    tidyverse,                    with:
    tidyverse/tidytemplate          extra-packages: pkgdown
                                    needs: website

Package websites and continuous integration are discussed more in Chapter 20 and Section 21.3, respectively.

The Config/Needs/* convention is handy because it allows a developer to use DESCRIPTION as their definitive record of package dependencies, while maintaining a clean distinction between true runtime dependencies versus those that are only needed for specialized development tasks.

12.9 Exports

For a function to be usable outside of your package, you must export it. When you create a new package with usethis::create_package(), nothing is exported at first, even once you add some functions. You can still experiment interactively with load_all(), since that loads all functions, not just those that are exported. But if you install and attach the package with library(pkg) in a fresh R session, you’ll notice that no functions are available.

12.9.1 What to export

Export functions that you want other people to use. Exported functions must be documented, and you must be cautious when changing their interface — other people are using them! Generally, it’s better to export too little than too much. It’s easy to start exporting something that you previously did not; it’s hard to stop exporting a function because it might break existing code. 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.

We believe that packages that have a wide audience should strive to do one thing and do it well. All functions in a package should be related to a single problem (or a set of closely related problems). Any functions not related to that purpose should not be exported. For example, most of our packages have a utils.R file (Section 7.2) that contains small utility functions that are useful internally, but aren’t part of the core purpose of those packages. We don’t export such functions. There are at least two reasons for this:

  • Freedom to be less robust and less general. A utility for internal use doesn’t have to be implemented in the same way as a function used by others. You just need to cover your own use case.

  • Regrettable reverse dependencies. You don’t want people depending on your package for functionality and functions that are unrelated to its core purpose.

That said, if you’re creating a package for yourself, it’s far less important to be this disciplined. Because you know what’s in your package, it’s fine to have a local “miscellany” package that contains a hodgepodge of functions that you find useful. But it is probably not a good idea to release such a package for wider use.

12.9.2 Re-exporting

Sometimes you want to make something available to users of your package that is actually provided by one of your dependencies. When devtools was split into several smaller packages (Section 3.2), many of the user-facing functions moved elsewhere. For usethis, the chosen solution was to list it in Depends (Section 11.5.1), but that is not a good general solution. Instead, devtools now re-exports certain functions that actually live in a different package.

Here is a blueprint for re-exporting an object from another package, using the session_info() function as our example:

  1. List the package that hosts the re-exported object in Imports in DESCRIPTION.1 In this case, the session_info() function is exported by the sessioninfo package.

    Imports:
        sessioninfo
  2. In one of your R/*.R files, have a reference to the target function, preceded by roxygen tags for both importing and exporting.

    #' @export
    #' @importFrom sessioninfo session_info
    sessioninfo::session_info

That’s it! Next time you re-generate NAMESPACE, these two lines will be there (typically interspersed with other exports and imports):

...
export(session_info)
...
importFrom(sessioninfo,session_info)
...

And this explains how library(devtools) makes session_info() available in the current session. This will also lead to the creation of the man/reexports.Rd file, which finesses the requirement that your package must document all of its exported functions. This help topic lists all re-exported objects and links to their primary documentation.


  1. Remember usethis::use_package() is helpful for adding dependencies to DESCRIPTION.↩︎