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 './'.
#> ✔ 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.
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
subdirectorywithr::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
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.
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::makeOxyFile(input = pkg_dir_R, overwrite = TRUE, verbose = FALSE)
#> ! `dir.out` is ignored when `overwrite` is `TRUE`
#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
}
#' @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.
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
#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'