28 January 2016

Objectives:

Build a package with a few demo functions using common Software Engineering Practices:

  • use devtools to create and check/build
  • use testthat for unit tests
  • use roxygen2 for (in-source) documentation

Why (write a package)?

The real question is, "why not?"

  • you're writing a script that takes no parameters
  • you're writing a script that's a few lines of ad-hoc analysis
  • you're writing experimental code

Our demo function:

extractPhoneNumbers() – we want to get this in a package:

extractPhoneNumbers <- function(inputStr) {
  # imports
  `%>%` <- stringr::`%>%`
  inputStr %>% stringr::str_replace_all(pattern = "[()+\\-_. ]",
                                    replacement = "") %>%
    stringr::str_extract_all(pattern = "[:digit:]{10}")
}

We're need:

Initializing our package:

bestPracticesWalkThroughL6-L21:

# set the package's info -- the first two settings should
# probably go in your .Rprofile
options(
  devtools.desc.author = '"Guy Fawkes <guy@fawkes.net> [aut,cre]"',
  devtools.desc.license = "GPL-3",
  devtools.desc = list( # set other package info:
    Version = "0.0.1",
    Title = "Demo Package",
    Description = "This package is supposed to demonstrate \
    the basics of package development with devtools & roxygen2...")
)

devtools::create(
  path = pkgName <- "demoPkg", # TERRIBLE FORM...
  rstudio = FALSE,             # remove auxiliary files
  check = TRUE)                # validate your DESCRIPTION parameters

devtools::create() output

files & structure:

    demoPkg/
        + DESCRIPTION  # properly filled
        + NAMESPACE    # (properly) empty
        + R/           # empty
        + man/         # empty

devtools::create() output

demoPkg/DESCRIPTION:

Package: demoPkg
Title: Demo Package
Version: 0.0.1
Authors@R: "Guy Fawkes <guy@fawkes.net> [aut,cre]"
Description: This package is supposed to demonstrate
    the basics of package development with devtools & roxygen2...
Depends:
    R (>= 3.2.3)
License: GPL-3
LazyData: true
RoxygenNote: 5.0.1

This is how R knows:

  • what packages demoPkg depends on (either Depends, Imports, or Suggestsdevtool's DESCRIPTION is a good example of all three.
  • package depends don't need to be fine grained (e.g. R (>= 3.2.3)) but this is best practice.

devtools::create output

NAMESPACE:

# Generated by roxygen2: do not edit by hand

This is how R knows:

  • what functions your package exports via ::. E.g. devtools::create. (@export)
  • what functions/packages your packages imports. (@import[From])
  • all of these things are managed with roxygen2

roxygen2: documentation engine

introduction:

  • From the DESCRIPTION:

    A 'Doxygen'-like in-source documentation system for Rd, collation, and 'NAMESPACE' files.

  • Really does marry source code and documentation:

    #' @title short description
    #' @description longer description
    #' @param x describe parameter x here
    #' @returns describe the function's output
    #' @export
    #' @importFrom pkgX fxnY
    #' @examples
    #' # put your example code here
    someFunction <- function(x) {
      return(NULL)
    }

roxygen2 syntax and notes

  • roxygen2 won't recognize "roxumentation" unless it starts with #'.
    • # (without the ') is just a regular R comment!!!
  • roxygen2 looks for "tags" (e.g. @title, @param, etc.) and processes everything after the tag (inside the #') until another tag is found.
  • a function isn't properly roxumented without @title, and @description:
    • they technically don't need to be specified (see Basic Documentation),
    • @description cannot be the same as @title
  • running roxygen2::roxygenise() will convert roxumentation in R/.*\.R into valid .Rd files.

roxygen2 comments

  • Even if you never package up a script you're working on you should roxument your script's functions!
  • The roxygen2 vignettes – written by Hadley Wickham – are fantastic for your standard needs.
  • The Templates section of my gist elaborates on and demonstrates @template[Var] (power-user tags).

PAUSE!

> best practices:

extractPhoneNumbers.RL1-L27 snippet:

#' @title Extract Phone Numbers
#'
#' @description Search for and extract all 10-digit phone numbers
#' in a string, provided the phone numbers are visually delimited
#' with the typical characters (i.e. '(', ')', '.', '+', '-', or ' ').
#'
#' @param inputStr a character vector (supposedly) containing phone
#' numbers to be extracted.
#'
#' @return a list of length equal to \code{inputStr} whose entries
#' contain character vectors whose entries are all extracted phone
#' numbers from the individual entries in \code{inputStr} (if any).
#'
#' @importFrom stringr %>% str_replace_all str_extract_all
#' @export
  • run devtools::document() to update .Rd's and NAMESPACE…

NAMESPACE:

# Generated by roxygen2: do not edit by hand

export(extractPhoneNumbers)
importFrom(stringr,"%>%")
importFrom(stringr,str_extract_all)
importFrom(stringr,str_replace_all)

imports & dependencies:

  • extractPhoneNumbers() imports some functions from stringr – let's make those explicit:

    # demoPkg/R/extractPhoneNumbers.R
    # ... SNIP ...
    extractPhoneNumbers <- function(inputStr) {
        # imports
        `%>%` <- stringr::`%>%`
        str_replace_all <- stringr::str_replace_all
        str_extract_all <- stringr::str_extract_all
        # ... SNIP ...
  • We need to update our package dependencies accordingly:

    devtools::use_package('stringr', type='Imports', pkg=pkgName) 
  • use_package is idempotent!

Updated DESCRIPTION:

Package: demoPkg
Title: Demo Package
Version: 0.0.1
Authors@R: "Guy Fawkes <guy@fawkes.net> [aut,cre]"
Description: This package is supposed to demonstrate
    the basics of package development with devtools & roxygen2...
Depends: 
    R (>= 3.2.3)
License: GPL-3
LazyData: true
RoxygenNote: 5.0.1
Imports: stringr
  • We're going to want to explicitly state which version of stringr:

    Imports: stringr (>= 1.0.0)

testthat

unit testing

unit testing overview

A lot has been written on (unit) testing:

testthat example

  • We can setup the testthat infrastructure with:

    devtools::use_testthat(pkg = pkgName)
    # create empty test file, test-extractPhoneNumbers
    devtools::use_test("extractPhoneNumbers.R", pkg = pkgName)
  • Fill test-extractPhoneNumbers.R:

    # demoPkg/tests/testthat/test-extractPhoneNumbers.R
    context("extractPhoneNumbers")
    
    test_that("expected cases are properly extracted", {
        testStrings <- c("1234567890", "123 456 7890")
        expectedOutput <- list("1234567890", "1234567890")
        expect_equal(extractPhoneNumbers(testStrings), expectedOutput)
    })
  • Using @examples as unit tests

Recap:

  • Created a package for our function
  • Roxumented our function
    • Didn't check that the documentation looked right
  • Updated DESCRIPTION and NAMESPACE
  • Wrote some unit tests
    • Didn't check to see that the function passes tests
  • Didn't check that our package can even build/install

Checking things

We can check:

  • documentation with devtools::dev_help()
  • unit tests with devtools::test()
  • build with devtools::check()
  • install with devtools::install()

Live Demo:

  • We're going to simulate building a package via the gist bestPracticesWalkThrough.R
  • You can execute the entire gist inside the interpreter:

    gistURL <- 'https://gist.github.com/stevenpollack/'
    gistURL <- paste0(gistURL, '141b14437c6c4b071fff')
    devtools::source_gist(gistURL)

References & Advanced Topics:

Packages, in general:

Roxygen

Depends/Imports/Suggests:

Q&A

Don't be shy!

devtools – typical workflow

devtools is setup in a such a way that you'll find yourself going through the following pattern/workflow:

  1. Fire up your IDE or text editor + interpreter and turn on dev_mode().
  2. Move into your package directory (setwd())
  3. Load your package into your environment (load_all())
  4. Make some changes to your code:
    1. Check that your (unit)tests pass. (test())
    2. Update and check that your roxumentation builds (document())
    3. Update your DESCRIPTION (e.g. version numbers and dependencies)
    4. Check that your package still builds and passes R CMD check (check())
  5. Test that the updated package installs (install())
  6. Repeat steps 3-5 as necessary
  7. Turn off dev_mode() and install updated package into original library.