10 Desenvolvimento de pacotes
Este capítulo foi inicialmente escrito por mim em inglês, como um post de blog. No futuro, o traduzirei.
10.1 Introdução
Esse capítulo é uma versão em html de meu script de desenvolvimento de pactotes. Não é altamente explicativo, mas apresenta as funções e o flow de desenvolvimento que utilizo.
This script provides an organization of the several utility functions for developing R packages provided by devtools and friends.
It conditions the usage of best practices as recommended by the devtools family of packages and the books “R Packages (2e)” by Hadley Wickham and “rOpenSci Packages: Development, Maintenance, and Peer Review” by the rOpenSci software review editorial team.
The script is organized from end to beggining:
- Set the package up (basically run once per package).
- update DESCRIPTION, dependencies, version, and releases.
- rebuilding and testing the package.
- creating new files and a blank canvas for informal debugging. Not shown here.
- Appendix:
- Function documentation.
- Other tips for package development, including packages you should use.
- Utility functions used in the script.
10.2 Setup
10.2.1 Name and Creation
available::available("predicater")
usethis::create_package("../predicater", check_name = FALSE)
# Also consider `create_tidy_package()`, but I prefer to set up manually10.2.2 Version Control
usethis::use_git() # Or manually via `git init`
# Setup github manually or via `use_github()`
usethis::use_github_links() # Adds links to DESCRIPTION
# Be wary about sensitive data, consider using `git_vaccinate()`
# Consider using the git/github helpers usethis::pr_* and the ones at
# (https://usethis.r-lib.org/reference/index.html#git-and-github)10.2.3 Documentation
usethis::use_mit_license() # Consider other licenses
usethis::use_readme_rmd()
usethis::use_news_md()
usethis::use_package_doc() # Then, populate this file with package-level docs
# If you have a logo, conisder `use_logo()`
usethis::use_lifecycle_badge("experimental")
# If your package is released in some repository, consider using the other
# badge functions10.2.4 Other Infrastructure
usethis::use_spell_check()
usethis::use_revdep()
usethis::use_lifecycle()
usethis::use_testthat()
usethis::use_pkgdown()
renv::install("ricardo-semiao/ricardo-semiao:themes/rspkgdown")
# Consider the content-related helpers:
# `use_cpp11()`, `use_c()` and friends, `use_make()`
# `use_pipe()`, `use_tibble()`10.2.6 CI/CD
usethis::use_coverage()
usethis::use_github_action("check-standard")
usethis::use_github_action("test-coverage")
# Consider the {air} for styling with `use_air()`. I prefer to do it manually
# If you want to use pkgdown with the standard Github pages scheme, consider
# using `use_pkgdown_github_pages()`10.2.7 Others
# - Helpers to set configurations options at
# (https://usethis.r-lib.org/reference/index.html#configuration)
# - Packages to switch practices: roxygen2md, prefixer, changer
# - Add a tutorial via `use_tutorial()`
# - If your package becomes big enough, consider adding a MAINTENANCE.md,
# a code of conduct (`use_code_of_conduct()`), a contributing guide
# (`use_tidy_contributing()`), and a citation file
# - Add a `use_cran_comments()` before submitting to CRAN
# - Add a cheatsheet
# Initial commit:
# At the end, check if no file is being committed/builded when it should not:
usethis::use_build_ignore(c("personal/", "predicater.code-workspace/"))10.3 Update Description, Packages, Version, and Releases
10.3.1 Versioning and Realeases
# Versioning:
desc <- desc::description$new()
desc$get_version()
desc$set_version(version)
desc$bump_version(which = c("patch", "minor", "major", "dev"))
# Releases:
devtools::submit_cran() # Also consider `release()`
usethis::use_github_release()10.3.2 Package imports
deps <- tibble::tribble( # Annotate dependencies here
~Type, ~Package, ~MinVersion,
"Depends", "R", "4.3",
"Imports", "rlang", NA, # Full import
"Imports", "vctrs", NA,
"Imports", "cli", NA,
"Imports", "glue", NA,
"Imports", "methods", NA,
"Suggests", "utils", NA
# Others included in commands i.e. thestthat
)
# purrr::walk(c("rlang", "purrr"), library, character.only = TRUE)
# cli_abort <- cli::cli_abort; cli_warn <- cli::cli_warn
# cli_inform <- cli::cli_inform; glue <- glue::glue
# Check the complexity of possible dependencies:
pak::pkg_deps_tree("pkg_you_are_thinking_to_use")
pak::pkg_deps_explain("pkg1", "pkg2")
itdepends::dep_weight(
setdiff(
subset(deps, Type == "Imports")$Package,
c("utils", "methods", "compiler")
)
)
# Dependencies:
use_packages(deps)
usethis::use_import_from("glue", "glue")
usethis::use_import_from("cli", c("cli_abort", "cli_warn", "cli_inform"))
#usethis::use_import_from("vctrs", c("vec_c"))
# Installation via renv:
renv::status()
renv::snapshot(packages = subset(deps, Type == "Imports")$Package)
renv::update(
packages = subset(deps, Type == "Imports")$Package |> setdiff("methods")
)
renv::restore()
renv::dependencies()$Package |> unique() |> sort()
# Consider adding standalone files via `use_standalone()`, `use_github_file()`10.3.3 DESCRIPTION file
desc <- desc::description$new()
# Main fields:
desc$set("Title",
"Modern and consistent validation for R"
)
desc$set("Description", glue::glue(
"Validation of R objects with more consistency and flexibility than base \\
'is' functions, having predictable and well documented behavior. \\
Provides simple 'is'/'are'/'has' predicates, and complex assertions for \\
common input validation needs. Includes improved object comparison, \\
prototypes, if-else chains, and pattern matching."
))
desc$set_authors(c(
person(
"Ricardo", "Semião", role = c("aut", "cre"),
email = "ricardo.semiao@outlook.com"
)
))
# Consider adding `comment = c(ORCID = "...")`
desc$add_urls("URL", "https://example.com")
# Mathjax, if required:
usethis::use_package("mathjaxr", type = "Imports")
desc$set("RdMacros", "mathjaxr")
desc$set("BuildManual", "TRUE")
# Finally, add `@importFrom mathjaxr preview_rd` somewhere
# Consider other fields (https://r-pkgs.org/description.html#other-fields)
# Writing: (maybe commit before)
desc$normalize()
desc$write()10.4 Rebuilding and Testing
10.4.1 Load and Document
devtools::document()
devtools::load_all() # Or maybe `reload()`
devtools::build_readme()
devtools::build_vignettes() # Uninstall with `clean_vignettes()`10.4.2 Essencial Tests
testthat::set_max_fails(Inf)
devtools::check_man()
devtools::test() # filter = "conditions"
devtools::run_examples()
devtools::check(check_dir = "personal/", args = c("--no-tests")) # "--no-examples"
revdepcheck::revdep_check()10.4.3 Installing and Building Site/Manual
devtools::install(upgrade = FALSE) # Unistall with `uninstall()`
devtools::build(path = "personal/")
devtools::build(path = "personal/", binary = TRUE)
devtools::build_manual(path = "personal")
pkgdown::clean_site(force = TRUE)
pkgdown::build_site(preview = FALSE)10.4.4 Additional Checks
# Coverage, spelling, and linting:
report_coverage <- covr::package_coverage(".")
covr::report(coverage_report)
report_spell <- spelling::spell_check_package(".")
report_spell
spelling::update_wordlist()
report_lint <- lintr::lint_package(".")
print(report_lint)
summarize_reports(report_lint, report_spell, report_lint)
# Override style - warning commit before using!
retport_style <- styler::style_pkg(
scope = "line_breaks",
style = styler::tidyverse_style,
strict = TRUE
)
print(retport_style)
# Development environment report, and others:
devtools::dev_sitrep()
devtools::missing_s3()
pak::pkg_deps_tree(".")10.4.5 Package Metadata
repo_data <- repometrics::repometrics_data(".")
pkg_data <- pkgstats::pkgstats()
pkg_data$loc |>
_[c("dir", "nfiles", "nlines", "ncode", "ndoc")] |>
subset(dir %in% c("R", "tests/testthat")) |>
as.data.frame() |>
(\(x) rbind(x, c(dir = "Total", colSums(x[-1]))))()
codemetar::write_codemeta(path = "personal/codemeta.json")10.5 Appendix: Function documentation
In general:
- Be concise and clear
- If the prompt includes more than one function, write a documentation for both in the same topic. Add the first function’s arguments first, then the unique ones of the second function, and so on
Create the sections:
- Title: in sentence case, omit the
@titletag - Description: add the
@descriptiontag. Use markdown lists when useful, specially in topics with more than one function - Parameters: add type hints as described below. If
...are sent to a function, cite it- For inherited arguments, prefer to add a simple, not-too-specific description, with a type hint, and cite the function. Use
@inheritParamswhen the function is in the same package
- For inherited arguments, prefer to add a simple, not-too-specific description, with a type hint, and cite the function. Use
- Return: use the
@returnstag. Add a description of what is returned -
@exportby default,@noRdif the function is internal, or otherwise if specified - Add an empty line between sections. Don’t add other sections, unless specifically requested
10.5.1 Type hints
General rules:
Add to
@paramand@valuetags, in the form of\[`ptype1` | `ptype2`]. Note the trailing\-
A ptype is a representation of an object basic metadata: typeof, class, and length/dimensions. There are four options:
- Prefer to use an expression that returns a prototype (see examples below), often a function call
- For specific classes, without simple prototype expressions, use
<class_name> - A literal value, e.g.
\[`"option1"` | `"option2"` | NULL],\[`TRUE` | `FALSE` | `NULL`] - A reference to a topic e.g.
[`rlang::args_data_masking`]to describe that...will be data masking dots. Note the lack of\in this case
-
General guidelines:
- Use the most specific ptype possible
- Avoid exceeding 30 characters, adding more details as the first thing in the description instead
Specific rules:
- For multiple arguments:
- For
...: add[`dynamic-dots`][rlang::dyn-dots]if the dots are passed tolist2(...)(or a function that does it);\[`ptype` each]if all the dots have the same ptype; or no type hint otherwise - For
@param arg1, arg2or@returnswith more than one function in the same topic, use\[`ptype1()`, `ptype2`] - If some argument have the same ptype as another argument
arg, use\[`=arg].
- For
- Variations:
- Lists or env with elements all of the same ptype can use
\[`list[ptype]`] - Use
|for union types, and&for intersection types. Shorthands: -
atomic()meaning the sum ptype of any atomic type -
anymeaning any R object -
numeric()meaning double or integer -
vector()meaning list, atomic, or expression vectors
- Lists or env with elements all of the same ptype can use
- Examples:
10.5.2 Formatting
- Use roxygen2 with markdown support: wrap code in backticks; math in dollar signs;
[pkg::fun()]to cite existing functions, etc. - Title in Sentence case
- Never remove whole lines, not even empty ones
- List: use
-, and do not add an empty line before the list block - In examples, denote the result of an code by
expr #> result(or with a line break for long results) - Line wraps and breaks:
- Always indent with one space before a line’s text
#', but not for empty lines (e.g. between sections) - Always wrap lines at 80 characters
- For inline tags, e.g.
@param, add two additional spaces after a line wrap, e.g.#'; for multiline tags e.g.@examples, don’t
- Always indent with one space before a line’s text
10.6 Appendix: Other Tips
Other package-related tips:
- Use renv (https://rstudio.github.io/renv/articles/packages.html) and maybe also {rig}.
- In the README, refer to {pak} or {remotes} for installation instructions, not {devtools}.
- Consider using the {config} package to manage environment-specific configurations, e.g. different settings for development and production. (https://rstudio.github.io/config/articles/config.html).
- To think about dependencies, start by defining which packages are very essencial and you could not reproduce their functionality. Then, their dependencies (and their dependencies, and so on) are also packages that you can’t avoid depending on, and thus its to also use as direct dependencies. See more on (https://r-pkgs.org/description.html#sec-description-imports-suggests-minium-version)
Other function-related tips:
- Creating custom checkers to reduce code repetition and improve consistency. Consider using {checkmate} for this.
- Use documentation helpers that run via inline roxygen code, to reduce code repetition and improve consistency.
- Add lifecycle badges to your functions. No need to repeat experimental if your whole package is experimental. Also use
deprecate_warn()and friends (https://lifecycle.r-lib.org/articles/communicate.html). - Use benchmarks and profiling: {bench}, {profvis}, {memtools}, {lobstr}, {log4r}, and {boomer}.
- Use {cli} for nice command-line outputs, and {progressbar}. Use {pillar} and {downlint} for syntax highlighting and linking.
- Try to add C/C++ code in performance-critical sections, preferably via {cleancall} and {cpp11}.
Opinionated paradigms:
- Use the {rlang} frameworks for metaprogramming, type-checking, and other high-quality utilities.
- Use {vctrs} for ptypes concept and coercion.
- Consider using S7 for OOP, or R6 if you need modify-in-place (often for performance).
- Use {purrr}, {slider}, and {rlist} for functional programming.
- For proeminent classes’ manipulation, consider {stringr}, {forcats}, {lubridate}, {hms} from the tidyverse; consider {bignum}, {sparsevctrs}, {bit}, {blob}, and {fastmap} for more complex objects.
- For data wrangling, consider {dplyr} + {tidyr}, but also more efficient alternatives like {data.table} or {collapse}, {r-polars}, {duckdb}, {sparklyr} {multidplyr}, note that all of them have transpilers from dplyr code.
Templates for .onLoad() and .onAttach():
.onAttach <- function(libname, pkgname) {
packageStartupMessage("Welcome to my package")
}
.onLoad <- function(libname, pkgname) {
rlang::run_on_load()
}Then use rlang::on_load() to register functions to be run at load time. Also consider creating a .onDetach function.
10.7 Appendix: Utility Functions
Function to update package dependencies to minimum major version available pkgs should be a data.frame. The third column can have the value “latest_major” to set the minimum version to the latest major version
use_packages <- function(deps) {
purrr::pwalk(deps, \(Type, Package, MinVersion) {
if (is.na(MinVersion)) {
MinVersion <- NULL
} else if (MinVersion == "latest_major") {
MinVersion <- gsub(
"([0-9]+)\\.[0-9]+\\.[0-9]+", "\\1.0.0", packageVersion(Package)
)
}
usethis::use_package(Package, type = Type, min_version = MinVersion)
})
}Function to summarize linting, coverage, and spelling reports into a single table and save it as a CSV file:
summarize_reports <- function(res_lint, res_covr, res_spell, path) {
# Relevant files:
filenames <- list(
man = list.files("man"),
R = list.files("R"),
vignettes = list.files("vignettes"),
c("README.Rmd", "NEWS.md", "DESCRIPTION") # Also consider LICENSE.md, LICENSE
) |>
unlist() |>
Filter(x = _, \(x) tools::file_ext(x) %in% c("R", "Rd", "Rmd", "qmd", "md", ""))
folders <- substr(names(filenames), 1, nchar(names(filenames)) - 1)
filenames <- filenames |>
strsplit("/") |>
vapply(character(1), FUN = \(x) tail(x, 1))
names(filenames) <- paste0(folders, "/", filenames)
# Lint, coverage, and spelling:
lint_data <- summary(res_lint)
lint_data <- rbind(
lint_data,
c(filenames = list("Total"), apply(lint_data[-1], 2, sum))
)
covr_data <- covr::coverage_to_list(res_covr)
covr_data <- data.frame(
file = c(names(covr_data$filecoverage), "Total"),
coverage = c(covr_data$filecoverage, covr_data$totalcoverage)
) |>
setNames(c("filenames", "coverage"))
spell_data <- sapply(res_spell$found, FUN = \(x) {
stringr::str_split_fixed(x, ":", 2)[, 1] |>
factor(levels = filenames) |>
table()
}) |>
apply(1, sum)
spell_data <- data.frame(
filenames = c(names(filenames), "Total"),
spell = c(spell_data, sum(spell_data))
)
# Merging all:
report <- lint_data |>
merge(covr_data, all = TRUE, by = "filenames") |>
merge(spell_data, all = TRUE, by = "filenames")
report <- report[match(c(names(filenames), "Total"), report$filenames), ] |>
`rownames<-`(NULL)
cat("Diagnostics report: (saved at", path, ")")
report |>
apply(2, \(x) ifelse(is.na(x), "", x)) |>
knitr::kable(align = "lccccc")
write.csv(report, path)
invisible(report)
}