Motivation

How can sinew help you?

sinew automates tasks that are part of R package documentation and maintance in order to help developers consistently create robust roxygen2 documentation and pass R CMD check --as-cran.

Quick Turnaraound

Two common scenarios arise in package development

  • You start a new project and open a file to develop your idea. Many functions later at the end of the day you look up and you have created a mess of a file.

  • You recieve a mammoth 10,000 line uncommented file to decipher for QC - good luck.

sinew can help turn the around that headache into a CRAN ready package in a few short steps

  1. Create a package project in RStudio.
  2. Place the file in the data-raw folder.
  3. Run sinew::untangle on the large script with the destination directory pkg/R. This will separate the core functions in the body into single function files (named as the function) and keep the body in body.R
  4. Run sinew::pretty_namespace to append namespaces in the function scripts.
  5. Run sinew::makeOxyFile to populate the Roxygen2 skeleton.
  6. Run sinew::make_import to populate the Imports field of the DESCRIPTION file.

This should get you far enough to make the impossible problem of understanding what is in that file to a manageable task, with the added benefit of producing a new package ready for distribution.

Reproducible Example

Lets use a reproducible example - The goal is to convert raw script into a package.

The script includes two functions yy and zz and some general script that uses them

To start we create a package with usethis

library(sinew)
pkg_dir <- file.path(tempdir(),'pkg')
usethis::create_package(path = pkg_dir, open = FALSE)
#> ✔ Creating './'.
#> ✔ Setting active project to "/tmp/RtmpiFEh89/pkg".
#> ✔ Creating 'R/'.
#> ✔ Writing 'DESCRIPTION'.
#> Package: pkg
#> Title: What the Package Does (One Line, Title Case)
#> Version: 0.0.0.9000
#> Authors@R (parsed):
#>     * First Last <[email protected]> [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.3.2
#> ✔ Writing 'NAMESPACE'.
#> ✔ Setting active project to "/tmp/RtmpiFEh89/pkg".
withr::with_dir(pkg_dir, usethis::use_data_raw(open = FALSE))
#> ✔ Creating 'data-raw/'.
#> ✔ Adding "^data-raw$" to '.Rbuildignore'.
#> ✔ Writing 'data-raw/DATASET.R'.
#> ☐ Finish writing the data preparation script in 'data-raw/DATASET.R'.
#> ☐ Use `usethis::use_data()` to add prepared data to package.
withr::with_dir(pkg_dir, usethis::use_mit_license(copyright_holder = "John Doe"))
#> ✔ Adding "MIT + file LICENSE" to 'License'.
#> ✔ Writing 'LICENSE'.
#> ✔ Writing 'LICENSE.md'.
#> ✔ Adding "^LICENSE\\.md$" to '.Rbuildignore'.
withr::with_dir(pkg_dir, usethis::use_roxygen_md())
withr::with_dir(pkg_dir,fs::dir_tree())
#> .
#> ├── DESCRIPTION
#> ├── LICENSE
#> ├── LICENSE.md
#> ├── NAMESPACE
#> ├── R
#> └── data-raw
#>     └── DATASET.R

Lets create a temporary file that will contain the script.


example_file <- tempfile(fileext = '.R',tmpdir = file.path(pkg_dir, 'data-raw'))

example_lines <- "#some comment
yy <- function(a=4){
  head(runif(10),a)
  # a comment
}

zz <- function(v=10,a=8){
  head(runif(v),a)
}


yy(6)

zz(30,3)
"

cat(example_lines,file = example_file)

Untangling Files

One of the first tasks for new developers is to move from long scripts that are intertwined with functions and body code into single function files in a R subdirectory and a clean body script that is easier to read.

This task is probably a non-starter if you have more than a few hundered lines of code. This is where sinew::untangle can save you time. sinew::untangle will separate the long script into single function files in a subdirectory and keep the body script intact.

pkg_dir_R <- file.path(pkg_dir,'R')
sinew::untangle(file = example_file,
                dir.out = pkg_dir_R, 
                dir.body = file.path(pkg_dir, 'data-raw'))

As we can see we got three new files.

  • body.R in the data-raw directory
  • yy.R in the R subdirectory
  • zz.R in the R subdirectory
withr::with_dir(pkg_dir,fs::dir_tree())
#> .
#> ├── DESCRIPTION
#> ├── LICENSE
#> ├── LICENSE.md
#> ├── NAMESPACE
#> ├── R
#> │   ├── yy.R
#> │   └── zz.R
#> └── data-raw
#>     ├── DATASET.R
#>     ├── body.R
#>     └── file128a19a7bc51.R
Click to see body.R


yy(6)
zz(30,3)


Click to see yy.R

#some comment
yy <- function(a=4){
  head(runif(10),a)
  # a comment
}


Click to see zz.R

zz <- function(v=10,a=8){
  head(runif(v),a)
}


Namespacing

It has become common practice to use the namespace in function calls, and it is obligatory in order to pass a cran check. But, not everyone does it and if you’re not use to it, it’s a pain to go back and update your script.

This is where sinew::pretty_namespace comes in. This function will go through your script and attach namespaces for you, with the same logic as the search path.

sinew::pretty_namespace(pkg_dir_R,overwrite = TRUE)
#> 
#> functions changed in '/tmp/RtmpiFEh89/pkg/R/yy.R':
#> 
#> ✔: found, ✖: not found, (): instances, ☒: user intervention
#> 
#>  ✔ utils::head  (1) 
#>  ✔ stats::runif (1) 
#> 
#> 
#> functions changed in '/tmp/RtmpiFEh89/pkg/R/zz.R':
#> 
#> ✔: found, ✖: not found, (): instances, ☒: user intervention
#> 
#>  ✔ utils::head  (1) 
#>  ✔ stats::runif (1)

So now we have separate files with functions appropriatly associated with namespaces, and now we can add roxygen2 headers.

Documentation

Now we are ready to create the function documentation using roxygen2. We use sinew::makeOxygen to create a skeleton for roxygen2 documentation. This function returns a skeleton that includes title, description, return, import and other fields populated with information scraped from the function script. We can also run sinew::makeOxygen in batch mode using sinew::makeOxyfile.

sinew::sinew_opts$set(markdown_links = TRUE)
sinew::makeOxyFile(input = pkg_dir_R, overwrite = TRUE, verbose = FALSE)
#> ! `dir.out` is ignored when `overwrite` is `TRUE`
Click to see yy.R

#some comment
#' @title FUNCTION_TITLE
#' @description FUNCTION_DESCRIPTION
#' @param a PARAM_DESCRIPTION, Default: 4
#' @returns OUTPUT_DESCRIPTION
#' @details DETAILS
#' @examples 
#' \dontrun{
#' if(interactive()){
#'  #EXAMPLE1
#'  }
#' }
#' @seealso 
#'  [head][utils::head]
#'  [runif][stats::runif]
#' @rdname yy
#' @export 
#' @importFrom utils head
#' @importFrom stats runif
yy <- function(a=4){
  utils::head(stats::runif(10),a)
  # a comment
}


Click to see zz.R

#' @title FUNCTION_TITLE
#' @description FUNCTION_DESCRIPTION
#' @param v PARAM_DESCRIPTION, Default: 10
#' @param a PARAM_DESCRIPTION, Default: 8
#' @returns OUTPUT_DESCRIPTION
#' @details DETAILS
#' @examples 
#' \dontrun{
#' if(interactive()){
#'  #EXAMPLE1
#'  }
#' }
#' @seealso 
#'  [head][utils::head]
#'  [runif][stats::runif]
#' @rdname zz
#' @export 
#' @importFrom utils head
#' @importFrom stats runif
zz <- function(v=10,a=8){
  utils::head(stats::runif(v),a)
}


The premise of sinew::makeOxygen is to expand on the default skeleton in RStudio, so basic fields are in the output by default. Each field is given with a relevant placeholder giving a hint what is expected. The following is the meat add to these bones:

  • param default values:
    • If a default value is set for a function parameter it will be added to the end @param line.
  • import/importFrom
    • The package scrapes the script with make_import looking for declared namespaces to create the proper calls for @import and @importFrom which are placed at the bottom of the output.
  • seealso
    • linking to other packages is also taken care of when adding the field @seealso. Any functions that are included in @importFrom will have a link to them by default.

DESCRIPTION FILE

It is also important to update the package DESCRIPTION file Imports field. This can be done for you with sinew::make_import, by either creating a new Imports field or updating an existing one.

sinew::make_import(pkg_dir_R,format = 'description',desc_loc = pkg_dir)
#> Imports:
#>  stats,
#>  utils

Update documentation

An important part of maintaining a package is keeping the documentation updated. Using sinew::moga we can achieve this painlessly. sinew::moga runs the same underlying script as sinew::makeOxygen but appends new information found into the current roxygen2 header instead of creating a new one.

Lets say we updated yy.R to include another param and used another function from the stats package. So the roxygen2 header is now out of synch with the current script.

new_yy <- "#some comment
#' @title FUNCTION_TITLE
#' @description FUNCTION_DESCRIPTION
#' @param a numeric, set the head to trim from random unif Default: 4
#' @return OUTPUT_DESCRIPTION
#' @details DETAILS
#' @examples 
#' \\dontrun{
#' if(interactive()){
#'  #EXAMPLE1
#'  }
#' }
#' @seealso 
#'  \\code{\\link[utils]{head}}
#'  \\code{\\link[stats]{runif}}
#' @rdname yy
#' @export 
#' @author Jonathan Sidi
#' @importFrom utils head
#' @importFrom stats runif
yy <- function(a=4,b=2){
  x <- utils::head(stats::runif(10*b),a)
  stats::quantile(x,probs=.95)
  # a comment
}"


cat(new_yy, file = file.path(pkg_dir_R,'yy.R'))
moga(file.path(pkg_dir_R,'yy.R'),overwrite = TRUE)
#> #' @title FUNCTION_TITLE
#> #' @description FUNCTION_DESCRIPTION
#> #' @param a numeric, set the head to trim from random unif Default: 4
#> #' @param b PARAM_DESCRIPTION, Default: 2
#> #' @return OUTPUT_DESCRIPTION
#> #' @details DETAILS
#> #' @examples 
#> #' \dontrun{
#> #' if(interactive()){
#> #'  #EXAMPLE1
#> #'  }
#> #' }
#> #' @seealso 
#> #'  \code{\link[utils]{head}}
#> #'  \code{\link[stats]{runif}}
#> #' @rdname yy
#> #' @export 
#> #' @author Jonathan Sidi
#> #' @importFrom utils head
#> #' @importFrom stats runif
#> #' @returns OUTPUT_DESCRIPTION
Click to see yy.R

#some comment
#' @title FUNCTION_TITLE
#' @description FUNCTION_DESCRIPTION
#' @param a numeric, set the head to trim from random unif Default: 4
#' @param b PARAM_DESCRIPTION, Default: 2
#' @return OUTPUT_DESCRIPTION
#' @details DETAILS
#' @examples 
#' \dontrun{
#' if(interactive()){
#'  #EXAMPLE1
#'  }
#' }
#' @seealso 
#'  \code{\link[utils]{head}}
#'  \code{\link[stats]{runif}}
#' @rdname yy
#' @export 
#' @author Jonathan Sidi
#' @importFrom utils head
#' @importFrom stats runif
#' @returns OUTPUT_DESCRIPTION
yy <- function(a=4,b=2){
  x <- utils::head(stats::runif(10*b),a)
  stats::quantile(x,probs=.95)
  # a comment
}


Oxygenize and Check

Oxygenize


r_env_vars <- function () {
    vars <- c(R_LIBS = paste(.libPaths(), collapse = .Platform$path.sep), 
        CYGWIN = "nodosfilewarning", R_TESTS = "", R_BROWSER = "false", 
        R_PDFVIEWER = "false")
    if (is.na(Sys.getenv("NOT_CRAN", unset = NA))) {
        vars[["NOT_CRAN"]] <- "true"
    }
    vars
}

withr::with_envvar(r_env_vars(), roxygen2::roxygenise(pkg_dir))
#> Writing 'NAMESPACE'
#> ℹ Loading pkg
#> Writing 'NAMESPACE'
#> Writing 'yy.Rd'
#> Writing 'zz.Rd'

R CMD Check

check_res <- rcmdcheck::rcmdcheck(path = pkg_dir, args = '--as-cran',quiet = TRUE)

Cleanup

unlink(pkg_dir, recursive = TRUE, force = TRUE)