From 3e2f36bc25e5a393ebbb02a30f9ecc44abee21a6 Mon Sep 17 00:00:00 2001 From: Ghislain Durif <gd.dev@libertymail.net> Date: Tue, 28 Feb 2023 12:49:38 +0100 Subject: [PATCH] presentation --- R_pkg_dev_helper.qmd | 399 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 R_pkg_dev_helper.qmd diff --git a/R_pkg_dev_helper.qmd b/R_pkg_dev_helper.qmd new file mode 100644 index 0000000..f283250 --- /dev/null +++ b/R_pkg_dev_helper.qmd @@ -0,0 +1,399 @@ +--- +title: "R helper packages to develop R packages" +author: "Ghislain Durif (<https://gdurif.perso.math.cnrs.fr/>)" +institute: "LBMC -- CNRS" +date: "2023/02/28" +toc: true +format: + html: + toc: true + execute: + echo: true + warning: false +--- + +## 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`](https://devtools.r-lib.org/) + +### Tools to Make Developing R Packages Easier • devtools + +```{r, eval=FALSE} +# 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() +``` + +#### Additional linls + +- [Package dev cheatsheet](https://devtools.r-lib.org/#cheatsheet) + +## [`usethis`](https://usethis.r-lib.org/) + +### Automate Package and Project Setup + +```{r, eval=FALSE} +# 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](#and-more)) + +#### Additional links: + +- [References](https://usethis.r-lib.org/reference/index.html) + +## [`fusen`](https://thinkr-open.github.io/fusen/) + +### Build a Package from Rmarkdown Files + +> "`{fusen}` inflates a Rmarkdown file to magically create a package." + + + +[Credit](https://thinkr-open.github.io/fusen/reference/figures/fusen_inflate_functions.png) + +```{r fusen, eval=FALSE} +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 + +- [`dev/(0-)dev_history.Rmd`](./examples/my.fusen.pkg/dev/dev_history.Rmd): recipe +- [`dev/flat_<flat_name1>.Rmd`](./examples/my.fusen.pkg/dev/flat_package.Rmd), `dev/flat_<flat_name2>.Rmd`, etc.: package content + + +#### Additional links + +- [FAQ](https://thinkr-open.github.io/fusen/articles/tips-and-tricks.html) + +## [`roxygen2`](https://roxygen2.r-lib.org/) + +### In-Line Documentation for R • roxygen2 + +File `R/add.R`: + +```{r, eval=FALSE} +#' 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()` +```Rd +% 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) +} +``` + +#### Additional link + +- [Getting started](https://roxygen2.r-lib.org/articles/roxygen2.html) + +## [`testthat`](https://testthat.r-lib.org/) + +### Unit Testing for R + +```{r, eval=FALSE} +# 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`: +```R +test_that("multiplication works", { + expect_equal(2 * 2, 4) +}) +``` + +Automatically run during package check (e.g. with `devtools::check()`) or specifically with `devtools::check()`. + +## [`checkmate`](https://mllg.github.io/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. + +```{r, error=TRUE} +checkmate::assert_choice("my-choice", choices = c("choice_1", "choice_2")) +``` + +#### Example ([source](https://mllg.github.io/checkmate/articles/checkmate.html)) + +Standard input check: +```{r, eval=FALSE} +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: +```{r, eval=FALSE} +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 + +```{r, error=TRUE} +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]") +``` + +#### Additional links + +- [Getting started](https://mllg.github.io/checkmate/articles/checkmate.html) +- [References](https://mllg.github.io/checkmate/reference/) + +## [`pkgdown`](https://pkgdown.r-lib.org/) + +### Build websites for R packages + +```{r, eval=FALSE} +# 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) + +```{r, eval=FALSE} +# 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 + +#### Additional links + +- [Getting started](https://pkgdown.r-lib.org/articles/pkgdown.html) + +## And more + +### [`covr`](https://covr.r-lib.org/): Test Coverage for Packages + +```{r, eval=FALSE} +usethis::use_coverage() +covr::package_coverage() +``` + +### [`gitlabr`](https://cran.r-project.org/package=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: + +```{r, eval=FALSE} +gitlabr::use_gitlab_ci( + image = "rocker/verse:latest", + type = "check-coverage-pkgdown" +) +``` + +### [`lintr`](https://lintr.r-lib.org/): A Linter for R Code + +```{r, eval=FALSE} +lintr::use_lintr(type = "tidyverse") +lintr::lint_package() +``` + +### [`withr`](https://withr.r-lib.org/): Run Code With Temporarily Modified Global State + +```{r, eval=FALSE} +getwd() +#> [1] "/home/runner/work/withr/withr/docs/reference" + +with_dir(tempdir(), { + # do some stuff + getwd() +}) +#> [1] "/tmp/RtmpR75In3" +``` -- GitLab