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
.
Two common scenarios arise in package development
sinew
can help turn the around that headache into a CRAN
ready package in a few short steps
RStudio
.data-raw
folder.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
sinew::pretty_namespace
to append namespaces in the function scripts.sinew::makeOxyFile
to populate the Roxygen2 skeleton.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.
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
pkg_dir <- file.path(tempdir(),'pkg')
usethis::create_package(path = pkg_dir, open = FALSE)
#> ✔ Creating '/tmp/RtmpbDkFPQ/pkg/'
#> ✔ Setting active project to '/tmp/RtmpbDkFPQ/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 <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.3.1
#> ✔ Writing 'NAMESPACE'
#> ✔ Setting active project to '<no active project>'
withr::with_dir(pkg_dir, usethis::use_data_raw(open = FALSE))
#> ✔ Setting active project to '/tmp/RtmpbDkFPQ/pkg'
#> ✔ Creating 'data-raw/'
#> ✔ Adding '^data-raw$' to '.Rbuildignore'
#> ✔ Writing 'data-raw/DATASET.R'
#> • Finish 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.
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 directoryyy.R
in the R subdirectoryzz.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
#> └── file2e003a05632f.R
Click to see body.R
yy(6)
zz(30,3)
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/RtmpbDkFPQ/pkg/R/yy.R':
#>
#> ✔: found, ✖: not found, (): instances, ☒: user intervention
#>
#> ✔ utils::head (1)
#> ✔ stats::runif (1)
#>
#>
#> functions changed in '/tmp/RtmpbDkFPQ/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.
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
line.@import
and @importFrom
which are placed at the bottom of the output.@seealso
. Any functions that are included in @importFrom
will have a link to them by default.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
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
}
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
check_res <- rcmdcheck::rcmdcheck(path = pkg_dir, args = '--as-cran',quiet = TRUE)
unlink(pkg_dir, recursive = TRUE, force = TRUE)