R helper packages to develop R packages

guide
Author
Affiliation

Ghislain Durif

LBMC – CNRS

Published

February 28, 2023

(sources)

pkg_list <- c(
    "codetools", "checkmate"
)

lapply(
    pkg_list, 
    function(pkg) {
        if(! pkg %in% .packages(all.available=TRUE))
            install.packages(pkg)
    }
)
[[1]]
NULL

[[2]]
NULL

Several R packages to help you develop R packages.

R package ?

my_package
├── .Rbuildignore
├── _pkgdown.yml
├── DESCRIPTION
├── LICENSE.md
├── LICENSE.note
├── NAMESPACE
├── README.md
├── README.Rmd
├── man
│   └── my_function.Rd
├── R
│   └── my_function.R
├── tests
│   ├── testthat
│   │   └── test-my_function.R
│   └── testthat.R
└── vignettes
    └── getting-started-with-my-function.Rmd

devtools

Tools to Make Developing R Packages Easier

# load local package without installing it
devtools::load_all()
# Build the documentation (man pages)
devtools::document()
# Run the unit tests
devtools::test()
# Run coverage test
devtools::test_coverage()
# Build package source
devtools::build()
# Run package check
devtools::check()

usethis

Automate Package and Project Setup

# Create a new package -------------------------------------------------
path <- file.path(tempdir(), "mypkg")
usethis::create_package(path)
#> ✔ Creating '/tmp/Rtmp4VMzwK/mypkg/'
#> ✔ Setting active project to '/private/tmp/Rtmp4VMzwK/mypkg'
#> ✔ Creating 'R/'
#> ✔ Writing 'DESCRIPTION'
#> Package: mypkg
#> Title: What the Package Does (One Line, Title Case)
#> Version: 0.0.0.9000
#> Authors@R (parsed):
#>     * First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)
#> Description: What the package does (one paragraph).
#> License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
#>     license
#> Encoding: UTF-8
#> Roxygen: list(markdown = TRUE)
#> RoxygenNote: 7.2.0
#> ✔ Writing 'NAMESPACE'
#> ✔ Setting active project to '<no active project>'
# only needed since this session isn't interactive
usethis::proj_activate(path)
#> ✔ Setting active project to '/private/tmp/Rtmp4VMzwK/mypkg'
#> ✔ Changing working directory to '/tmp/Rtmp4VMzwK/mypkg/'

# Modify the description ----------------------------------------------
usethis::use_mit_license("My Name")
#> ✔ Setting License field in DESCRIPTION to 'AGPL (>= 3)'
#> ✔ Writing 'LICENSE.md'
#> ✔ Adding '^LICENSE\\.md$' to '.Rbuildignore'

usethis::use_package("ggplot2", "Suggests")
#> ✔ Adding 'ggplot2' to Suggests field in DESCRIPTION
#> • Use `requireNamespace("ggplot2", quietly = TRUE)` to test if package is installed
#> • Then directly refer to functions with `ggplot2::fun()`

# Set up other files -------------------------------------------------
usethis::use_readme_md()
#> ✔ Writing 'README.md'
#> • Update 'README.md' to include installation instructions.

usethis::use_news_md()
#> ✔ Writing 'NEWS.md'

usethis::use_test("my-test")
#> ✔ Adding 'testthat' to Suggests field in DESCRIPTION
#> ✔ Setting Config/testthat/edition field in DESCRIPTION to '3'
#> ✔ Creating 'tests/testthat/'
#> ✔ Writing 'tests/testthat.R'
#> ✔ Writing 'tests/testthat/test-my-test.R'
#> • Edit 'tests/testthat/test-my-test.R'

x <- 1
y <- 2
usethis::use_data(x, y)
#> ✔ Adding 'R' to Depends field in DESCRIPTION
#> ✔ Creating 'data/'
#> ✔ Setting LazyData to 'true' in 'DESCRIPTION'
#> ✔ Saving 'x', 'y' to 'data/x.rda', 'data/y.rda'
#> • Document your data (see 'https://r-pkgs.org/data.html')

# Use git ------------------------------------------------------------
usethis::use_git()
#> ✔ Initialising Git repo
#> ✔ Adding '.Rproj.user', '.Rhistory', '.Rdata', '.httr-oauth', '.DS_Store' to '.gitignore'

Other

  • usethis::use_devtools()
  • usethis::use_build_ignore("<filename>") to add files to .Rbuildignore
  • usethis::use_git_ignore("<filename>") to add files to .gitignore
  • usethis::use_github_action() or usethis::use_gitlab_ci() to setup a CI (c.f. gitlabr package below)

fusen

Build a Package from Rmarkdown Files

{fusen} inflates a Rmarkdown file to magically create a package.”

Credit

path <- file.path(getwd(), "examples", "my.fusen.pkg")
fusen::create_fusen(path, template = "full", open = FALSE)
#> ── Creating new directory: /path/to/examples/my.fusen.pkg ──────────────────────────────────────────────────────────────────────────────────────────────────
#> ✔ Creating '/path/to/examples/my.fusen.pkg/'
#> ✔ Setting active project to '/path/to/examples/my.fusen.pkg'
#> ✔ Creating 'R/'
#> ✔ Writing a sentinel file '.here'
#> • Build robust paths within your project via `here::here()`
#> • Learn more at <https://here.r-lib.org>
#> ✔ Setting active project to '<no active project>'
#> ✔ New directory created: /path/to/examples/my.fusen.pkg
#> ── Adding dev/flat_full.Rmd ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> File .here already exists in /path/to/examples/my.fusen.pkg
#> ✔ Added /path/to/examples/my.fusen.pkg/dev/flat_full.Rmd, /path/to/examples/my.fusen.pkg/dev/0-dev_history.Rmd
fusen::add_flat_template(path, flat_name = "package")
#> '0-dev_history.Rmd' already exists. It was not overwritten. Set `add_flat_template(overwrite = TRUE)` if you want to do so.
#> File .here already exists in /path/to/examples/my.fusen.pkg
#> • Modify '/path/to/examples/my.fusen.pkg/dev/flat_package.Rmd'

Package development

roxygen2

In-Line Documentation for R

File R/add.R:

#' Add together two numbers
#'
#' @param x A number.
#' @param y A number.
#' @return A number.
#' @examples
#' add(1, 1)
#' add(10, 1)
add <- function(x, y) {
  x + y
}

Automatic generation of man/add.Rd with devtools::document():

% Generated by roxygen2: do not edit by hand
% Please edit documentation in ./<text>
\name{add}
\alias{add}
\title{Add together two numbers}
\usage{
add(x, y)
}
\arguments{
\item{x}{A number.}

\item{y}{A number.}
}
\value{
A number.
}
\description{
Add together two numbers
}
\examples{
add(1, 1)
add(10, 1)
}

testthat

Unit Testing for R

# run once to configure the package meta-data
usethis::use_testthat()
# to create a test file "tests/testthat/test-my_function.R"
usethis::use_test("my_function")
#> ✔ Setting active project to '/home/drg/work/dev/R/funStatTest'
#> ✔ Writing 'tests/testthat/test-my_function.R'
#> • Modify 'tests/testthat/test-my_function.R'

File tests/testthat/test-my_function.R:

test_that("multiplication works", {
  expect_equal(2 * 2, 4)
})

Automatically run during package check (e.g. with devtools::check()) or specifically with devtools::test().

checkmate

Fast and versatile argument checks for R

  • assert_xxx() fails if assertion not met
  • test_xxx() returns a the check result as a logical (TRUE/FALSE) value
  • expect_xxx() are designed to be used in testthat unit tests.
checkmate::assert_choice("my-choice", choices = c("choice_1", "choice_2"))
Error in eval(expr, envir, enclos): Assertion on '"my-choice"' failed: Must be element of set {'choice_1','choice_2'}, but is 'my-choice'.

Example (source)

Standard input check: ::: {.cell}

fact <- function(n, method = "stirling") {
  if (length(n) != 1)
    stop("Argument 'n' must have length 1")
  if (!is.numeric(n))
    stop("Argument 'n' must be numeric")
  if (is.na(n))
    stop("Argument 'n' may not be NA")
  if (is.double(n)) {
    if (is.nan(n))
      stop("Argument 'n' may not be NaN")
    if (is.infinite(n))
      stop("Argument 'n' must be finite")
    if (abs(n - round(n, 0)) > sqrt(.Machine$double.eps))
      stop("Argument 'n' must be an integerish value")
    n <- as.integer(n)
  }
  if (n < 0)
    stop("Argument 'n' must be >= 0")
  if (length(method) != 1)
    stop("Argument 'method' must have length 1")
  if (!is.character(method) || !method %in% c("stirling", "factorial"))
    stop("Argument 'method' must be either 'stirling' or 'factorial'")

  if (method == "factorial")
    factorial(n)
  else
    sqrt(2 * pi * n) * (n / exp(1))^n
}

:::

checkmate-based input check: ::: {.cell}

fact <- function(n, method = "stirling") {
  assertCount(n)
  assertChoice(method, c("stirling", "factorial"))

  if (method == "factorial")
    factorial(n)
  else
    sqrt(2 * pi * n) * (n / exp(1))^n
}

:::

Advanced checks

x <- runif(100)
y <- rnorm(100)
# expect 100 numerical values with NA and between 0 and 1
checkmate::qassert(x, "N100[0,1]")
checkmate::qassert(y, "N100[0,1]")
Error in eval(expr, envir, enclos): Assertion on 'y' failed. All elements must be >= 0.

pkgdown

Build websites for R packages

# Run once to configure package to use pkgdown
usethis::use_pkgdown()

Config file: _pkgdown.yml (see the https://pkgdown.r-lib.org/articles/customise.html)

# Run to build the website
pkgdown::build_site()
  • README.md -> home page index.html
  • vignettes -> “article” pages
  • man/*.Rd -> “reference” pages
  • NEWS.md -> “News” pages
  • DESCRIPTION metadata file -> home page side bar with links and description

And more

covr: Test Coverage for Packages

usethis::use_coverage()
covr::package_coverage()

gitlabr: Access to the ‘Gitlab’ API

Setup a Gitlab CI to check the package, check the test coverage and deploy a pkgdown website as Gitlab pages:

gitlabr::use_gitlab_ci(
    image = "rocker/verse:latest",
    type = "check-coverage-pkgdown"
)

lintr: A Linter for R Code

lintr::use_lintr(type = "tidyverse")
lintr::lint_package()

withr: Run Code With Temporarily Modified Global State

getwd()
#> [1] "/home/runner/work/withr/withr/docs/reference"

with_dir(tempdir(), {
    # do some stuff
    getwd()
})
#> [1] "/tmp/RtmpR75In3"