workflows using .Rmd documents in the vignettes/ folder
Software Engineering approach
Following conventions allows us to make use of automated tools for:
Checking and testing code
Producing documentation for code and workflows
Publishing, distributing and citing code
Anatomy of an R package
Letβs use pkgreviewr, a package I authored to help automate some aspects of the rOpenSci review process, as an example to examine some elements of what makes a package:
DESCRIPTION file
Capture metadata around the package - Functionality description - Creators - License
Package: pkgreviewr
Type: Package
Title: rOpenSci package review project template
Version: 0.1.1
Authors@R: c(person("Anna", "Krystalli", email = "annakrystalli@googlemail.com",
role = c("aut", "cre")),
person("MaΓ«lle", "Salmon", email = "maelle.salmon@yahoo.se", role = "aut"))
Description: Creates files and collects materials necessary to complete an rOpenSci package review.
Review files are prepopulated with review package specific metadata. Review package source code is
also cloned for local testing and inspection.
License: GPL-3 + file LICENSE
URL: https://github.com/ropenscilabs/pkgreviewr
BugReports: https://github.com/ropenscilabs/pkgreviewr/issues
Encoding: UTF-8
LazyData: true
Imports:
devtools,
git2r (>= 0.23.0),
usethis (>= 1.2.0),
here,
reprex,
gh,
base64enc,
whoami,
magrittr,
covr,
goodpractice,
assertthat,
httr,
rstudioapi,
clipr,
clisymbols,
crayon,
dplyr,
glue,
fs,
urltools,
shiny
Suggests:
testthat,
mockery,
knitr,
rmarkdown
RoxygenNote: 6.1.1
Remotes:
ropensci/git2r
VignetteBuilder: knitr
Roxygen: list(markdown = TRUE)
To cite package 'pkgreviewr' in publications use:
Krystalli A, Salmon M (2021). _pkgreviewr: rOpenSci package review
project template_. R package version 0.3.1,
<https://github.com/ropensci-org/pkgreviewr>.
A BibTeX entry for LaTeX users is
@Manual{,
title = {pkgreviewr: rOpenSci package review project template},
author = {Anna Krystalli and MaΓ«lle Salmon},
year = {2021},
note = {R package version 0.3.1},
url = {https://github.com/ropensci-org/pkgreviewr},
}
Dependency management
Itβs the job of the DESCRIPTION to list the packages that your package needs to work.
Imports are necessary dependencies for the functions in your package to work
Suggests:
testthat,
mockery,
knitr,
rmarkdown
Suggests are dependencies that are not necessary for the functions in your package but might be neccessary to run all the vignettes or tests in your package
#' Add together two numbers.#'#' @param x A number.#' @param y A number.#' @return The sum of x and y.#' @examples#' add(1, 1)#' add(10, 1)add<-function(x, y){x+y}
tests/
Tests provide confidence in what the code is doing.
The R package structure can help with providing a logical organisation of files, by providing a set of standard locations for certain types of files.
To work with packages in RStudio we use the Build pane, which includes a variety of tools for building, documenting and testing packages. This will appear if Rstudio recognises the project as an R package.
π» Create your first package
Creating a new package locally
To create a new package, you can follow the steps for creating any new project, but this time select R package instead of New Project and call your new package mypackage.
File > New Project⦠> New Directory > R package > mypackage
Otherwise, you can use function usethis::create_package("mypackage") to create a new package. The argument we provide is the path to here we want our new package created, the last element being the package name. Note that there are restrictions on what characters can be used in a package name.
Copy mypackage Project
To make things easier, Iβve gone ahead and set up the basic contents of a new package in project mypackage.
In our shared space click on the copy button next to the mypackage Project to copy it.
Your new project should have the following structure.
.
βββ DESCRIPTION
βββ NAMESPACE
βββ R
βββ project.Rproj
Initialise git and commit files.
Letβs also configure git again, initialise our project as a git repository and commit our initial files. Weβll need to configure git again as this is a new Posit Cloud project.
β Setting active project to '/cloud/project/mypackage'
β Initialising Git repo
β Adding '.Rhistory', '.Rdata', '.httr-oauth', '.DS_Store', '.quarto' to '.gitignore'
There are 5 uncommitted files:
* '.gitignore'
* '.Rbuildignore'
* 'DESCRIPTION'
* 'mypackage.Rproj'
* 'NAMESPACE'
Is it ok to commit them?
1: No way
2: Nope
3: I agree
Selection: 3
β Adding files
β Making a commit with message 'Initial commit'
π¦ Functions in the R/ dir
Create function script and first function
Letβs create a script and write our first function:
ββ R CMD build ββββββββββββββββββββββββββββββββββββββββββββ
β checking for file β/cloud/project/mypackage/DESCRIPTIONβ ...
β preparing βmypackageβ:
β checking DESCRIPTION meta-information
β checking for LF line-endings in source and make files and shell scripts
β checking for empty or unneeded directories
β building βmypackage_0.0.0.9000.tar.gzβ
Warning: invalid uid value replaced by that for user 'nobody'
Running /opt/R/4.3.3/lib/R/bin/R CMD INSTALL \
/tmp/RtmpVJXWXx/mypackage_0.0.0.9000.tar.gz \
--install-tests
* installing to library β/cloud/lib/x86_64-pc-linux-gnu-library/4.3β
* installing *source* package βmypackageβ ...
** using staged installation
** R
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded from temporary location
** testing if installed package can be loaded from final location
** testing if installed package keeps a record of temporary installation path
* DONE (mypackage)
Oooops? Our function doesnβt seem to be available! π€
Letβs try something else:
mypackage:::hello()
[1] "Hello, world!"
So whatβs happened here?
Although the package contains our function, we have not exported it to make it available to a user.
Currently it is contained as an internal function. Hence we can access it with the notation for internal functions (:::) but not directly when we load the package!
To export the function, we will need to add a tag to tell R to do so using a special documentation notation called Roxygen!
π¦ Add Roxygen documentation
Roxygen2 allows you to write specially-structured comments preceding each function definition to document:
a description of what it does
the inputs and outputs
an example of how to use it
whether to export the function or treat it as internal
These are processed automatically to produce .Rd help files for your functions and control which functions are exported to the package NAMESPACE.
Letβs document our example function.
Insert Roxygen skeleton
You can insert a Roxygen skeleton by placing the cursor within a function and clicking:
This re-creates a hello.Rd help file in the man/ folder and populates the NAMESPACE with our functions
Configure Build Tools
We can also configure our Build Tools to run devtools::document() every time we re-install the package.
To do so, we click on the Build > Configure Build Tools > Generate documentation with Roxygen > Configure⦠and then tick the Install and Restart box.
Now clicking Install in the Build panel will also build your docs in the man/ folder for you.
Now that the documentation has been built, the help file for our hello() function is now available!
π¦ Personalise function
Letβs go a step further and customise our function so that the greeting is from ourselves!
#' Hello World!#'#' Print hello greeting#' @return prints hello greeting to console from me#' @export#'#' @examples#' hello()hello<-function(){print("Hello, world from Anna")}
Add some fun!
Programming is most useful for having fun. So letβs make our function extra fun!
So letβs create a function that randomly chooses one of the animals available in cowsayto deliver the greeting, and also allow the user to customise who the recipient of the greeting is.
#' Hello World!#'#' Print personalised hello greeting from me.#'#' @param name character string. Your name!#'#' @return prints hello greeting to console#' @export#'#' @examples#' hello()#' hello("Lucy Elen")hello<-function(name=NULL){# create greetingif(is.null(name)){name<-"world"}greeting<-paste("Hello", name, "from Anna!")# randomly sample an animalanimal_names<-names(cowsay::animals)i<-sample(1:length(animal_names), 1)cowsay::say(greeting, animal_names[i])}
An important part of the package development process is R CMD check. R CMD check automatically checks your code and can automatically detects many common problems that weβd otherwise discover the hard way.
Both these run R CMD check which return three types of messages:
ERRORs: Severe problems that you should fix regardless of whether or not youβre submitting to CRAN.
WARNINGs: Likely problems that you must fix if youβre planning to submit to CRAN (and a good idea to look into even if youβre not).
NOTEs: Mild problems. If you are submitting to CRAN, you should strive to eliminate all NOTEs, even if they are false positives.
Letβs Check our package:
Click on the Check button (πβ )
ββ R CMD check results ββββββββββββββββββββββββββββββ mypackage 0.0.0.9000 ββββ
Duration: 8.4s
β― checking DESCRIPTION meta-information ... WARNING
Non-standard license specification:
What license is it under?
Standardizable: FALSE
β― checking dependencies in R code ... WARNING
'::' or ':::' import not declared from: βcowsayβ
0 errors β | 2 warnings β | 0 notes β
Error: R CMD check found WARNINGs
Execution halted
Exited with status 1.
Aha, so our checks have thrown up some warnings! First, itβs telling us we havenβt added a LICENSE. Itβs also telling us that we have a dependency (import) from package cowsay which we havenβt documented in the DESCRIPTION file. usethis to the rescue!
Testing is a vital part of package development. It ensures that our code does what you want it to do.
Once youβre set up with a testing framework, the workflow is simple:
Modify your code or tests.
Test your package with Ctrl/Cmd + Shift + T or devtools::test().
Repeat until all tests pass.
Create test file
To create a new test file (and the testing framework if required), use function usethis::use_test(). Itβs good practice to name the test files after the .R files containing the functions being tested.
β Adding 'testthat' to Suggests field in DESCRIPTIONβ Adding '3' to Config/testthat/editionβ Creating 'tests/testthat/'β Writing 'tests/testthat.R'β Writing 'tests/testthat/test-hello.R'β’ Modify 'tests/testthat/test-hello.R'
It also added testthat to the suggested packages in the DESCRIPTION file.
Suggests:
testthat
Thatβs because you donβt need test that to run the functions in mypackage, but you do if you want to run the tests.
When the tests are run (either through running devtools::test(), clicking on More > Test Package in the Build panel or Cmd/Ctrl + Shift + T), the code in each test script in directory testthat is run.
test-hello.R
Letβs load the library so we can explore the testthat testing framework
ββ Failure: multiplication works βββββββββββββββββββββββββββββββββββββββββββββββ
2 * 2 not equal to 5.
1/1 mismatches
[1] 4 - 5 == -1
Error:
! Test failed
Write test
Letβs write a simple test to check that we are getting an expected output type.
One way to test the consistency of the output of a function, especially one that may output messages, warnings etc, like ours does, is to use expect_snapshot().
This function takes a snapshot of the output of a function and compares it to the snapshot the next time the test is run.
Now, because our functions has a random element to it (selecting a random animal), we need to set the seed to ensure that the output is consistent across runs.
So letβsr eplace the placeholder testing code and add a snapshot test each for:
the functionβs default behaviour.
the functionβs behaviour when a name is provided.
The first time we run our tests, testthat will create a snapshot of the output of our function and warns us of that. The next time we run the tests, it will compare the output of the function to the snapshot and let us know if it has changed.
If you run your tests twice you should have success!
==> devtools::test()
βΉ Testing mypackage
β | F W S OK | Context
β | 2 | hello
ββ Results βββββββββββββββββββββββββββββββββββββββ
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 2 ]
Letβs commit our current files and changes.
π¦ Create README
The final document you will need for your package is a README.
β Setting active project to '/cloud/project'
β Writing 'README.Rmd'
β Adding '^README\\.Rmd$' to '.Rbuildignore'
β’ Modify 'README.Rmd'
β’ Update 'README.Rmd' to include installation instructions.
β Writing '.git/hooks/pre-commit'
.Rmd documents are the predecessors to Quarto documents and allow us to run R code in our GitHub README.
Because itβs an .Rmd but GitHub can only display an md document as itβs landing page, this is a special .Rmd that renders to a markdown document rather than html.
The function adds a check to .git to ensure you commit an up to date version on the md when you commit changes to the .Rmd.
Important elements of a README to include:
Installation instructions: In our case we will be distributing our package via GitHub, so we will include instructions on how to install the package from GitHub using remotes::install_github("<yourgithubusername>/mypackage").
Description: A brief description of what the package does.
Examples: A few simple examples of how to use the package.
Complete the README. Hereβs what mine looks like:
---output: github_document---<!-- README.md is generated from README.Rmd. Please edit that file --># mypackage<!-- badges: start --><!-- badges: end -->The goal of mypackage is to print a personalised greeting from me!## InstallationYou can install the development version of mypackage from GitHub with:``` r# install.packages("remotes")remotes::install_github("annakrystalli/mypackage")```## ExampleThis is a basic example which shows you how to print a generic greeting:```{r example}library(mypackage)## basic example codehello()```This is a basic example which shows you how to print a personalised greeting:```{r}hello(name = "Lucy Elen")```
and renders to the following when Knit
Letβs commit our current files and changes.
π¦ Complete package metadata
Letβs head to the DESCRIPTION file and complete the details.
Authors
First letβs complete the authors.
Remove the current author and maintainer lines and replace it with the following line:
Authors@R: person("First", "Last", email = "first.last@example.com", role = c("aut", "cre"))
completed with your own details
Add a title and description
Complete the title and description fields with appropriate details.
If you want to form a paragraph of text, make sure do indent the hanging lines by 4 spaces (one tab). And make sure that your Description field ends in a full-stop.
Add a date
Use todayβs date in ISO format, ie 2024-04-19. This will populate a citation entry for us.
Completed DESCRIPTION
The complete DESCRIPTION file should look something like this:
Package: mypackage
Title: Customised greetings from me!
Version: 0.0.0.9000
Authors@R: person("Anna", "Krystalli",
email = "annakrystalli@googlemail.com",
role = c("aut", "cre"))
Description: Prints a customised greeting from myself,
delivered by a friend.
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
Imports:
cowsay
Suggests:
testthat (>= 3.0.0)
Config/testthat/edition: 3
Date: 2024-04-19
Check your package. If all is good, document, install and restart!
To cite package βmypackageβ in publications use:
Krystalli A (2024). _mypackage: Customised
greetings from me!_. R package version 0.0.0.9000.
A BibTeX entry for LaTeX users is
@Manual{,
title = {mypackage: Customised greetings from me!},
author = {Anna Krystalli},
year = {2024},
note = {R package version 0.0.0.9000},
}
Letβs commit our current files and changes.
Create GitHub repo and push to GitHub
Now you have everything you need to share your package on GitHub so create a GitHub repository and push our local content up to it.
You will need to set your PAT through credentials again in this new project.
# configure GitHub PAT credentialscredentials::set_github_pat()# create GitHub repository and pushusethis::use_github(protocol ="https")
β Creating GitHub repository 'annakrystalli/mypackage'
β Setting remote 'origin' to 'https://github.com/annakrystalli/mypackage.git'
β Adding 'https://github.com/annakrystalli/mypackage' to URL
β Adding 'https://github.com/annakrystalli/mypackage/issues' to BugReports
There is 1 uncommitted file:
* 'DESCRIPTION'
Is it ok to commit it?
1: Absolutely
2: Not now
3: No way
Selection: 1
β Adding files
β Making a commit with message 'Add GitHub links to DESCRIPTION'
β Pushing 'master' branch to GitHub and setting 'origin/master' as upstream branch
β Opening URL 'https://github.com/annakrystalli/mypackage'
This will create a new GitHub repository and push the contents of your package to it.
Before doing so it also adds details of our remote to the DESCRIPTION file and commits it.
Now anyone will be able to install my package using, eg:
Visit someone elseβs package repo, follow the instructions to install, load it and receive a personalised greeting from the author!
Setup Continuous Integration with GitHub Actions
Continuous Integration Background
Continuous integration (CI) is the practice of automating the integration of code changes from multiple contributors into a single software project. Automated tools are used to assert the new codeβs correctness before integration. In our case this is what are tests are for. And GitHub Actions allows us to do this all within GitHub!
Once the appropriate CI workflow is set up, it will automatically run our tests for us every time we push new code to GitHub or when a new pull request is made. This way we can (try to) ensure we donβt merge new code into our code base that introduces bugs (causing our tests to fail).
Obviously, our ability to screen for breaking changes is only as good as our tests! But at least we can get GitHub to run our tests for us!
Setup R CMD CHECK GitHub Action
To run CI workflows through GitHub Actions, we specify the actions or jobs we want the CI system in a .yaml file that lives in .github/workflows/ folder in the root of our package/project.
This can be quite complicated for complex projects as you have to effectively specify instructions for GitHub to recreate a computational environment to run the tests in. The .yaml format can also be fiddly, with indents having special meaning.
However for a standard R project like ours, there is a usethis functions that can create and appropriate .yaml for us that works right out of the box!
Re-knit README.Rmd to include GitHub Actions badge.
Commit the whole .github folder as well as README.Rmd & README.md
Push to GitHub
Letβs commit our current files and changes and push to GitHub.
Youβll need your GitHub PAT again
This will activate the R-CMD-CHECK GitHub Actions workflow and begin running it.
To see details and the status of GitHub Actions workflows, click on the Actions tab and select the workflow to view.
If our test passes, the run will show as successful β and so will the badge in our README. If there are any problems, error messages will appear in the logs.
π¦ Create documentation site
While the README is a great first step for documenting your package, there is an easy way to create a full website which makes the documentation of each exported function avaliable online and where you could include additional, more detailed vignettes.
You can use package pkgdown to create an online site for your documentation. It effectively recycles the documentation you have already created for your functions, information in your README and DESCRIPTION file and presents it in a standardised website form.
-- Installing package into temporary library --------------
== Building pkgdown site =======================================================
Reading from: '/cloud/project'
Writing to: '/cloud/project/docs'
-- Initialising site -----------------------------------------------------------
Copying '../lib/x86_64-pc-linux-gnu-library/4.3/pkgdown/BS5/assets/link.svg' to 'link.svg'
Copying '../lib/x86_64-pc-linux-gnu-library/4.3/pkgdown/BS5/assets/pkgdown.js' to 'pkgdown.js'
-- Building home ---------------------------------------------------------------
Writing 'authors.html'
Reading 'LICENSE.md'
Writing 'LICENSE.html'
Writing 'LICENSE-text.html'
Writing '404.html'
-- Building function reference -------------------------------------------------
Writing 'reference/index.html'
Reading 'man/hello.Rd'
Writing 'reference/hello.html'
Writing 'sitemap.xml'
-- Building search index -------------------------------------------------------
== DONE ========================================================================
-- Previewing site ----------------------------------------
This creates html documentation for our package in the docs/ folder and presents you with a preview to the site.
Caution
Note the Preview might be blocked in Posit Cloud and when you click to allow a pop might still look a bit wonky!
Now, you could push this docs/ folder to GitHub and serve through GitHub Pages, but you would need to rebuild, commit and push these docs any time there was a change in the package documentation.
Instead what we could do is get our CI system (GitHub Actions) to build our docs for us automatically every time there is a push or pull request to our repository!! So letβs try this approach!
Indeed thatβs what the usethis::use_pkgdown() workflow expects so has already told git to ignore local copies of the docs in our docs/ folder by adding docs/* to our .gitignore file. This way, we can still build and preview our site locally if we want but we wonβt commit it to ourmaster or main branch.
Then, we can deploy another GitHub Actions template, provided by usethis that again will work right out the box!
β Saving 'r-lib/actions/examples/pkgdown.yaml@v2' to '.github/workflows/pkgdown.yaml'
β’ Learn more at <https://github.com/r-lib/actions/blob/v2/examples/README.md>.
This creates another file in .github/workflows/ called pkgdown.yaml. The workflow installs the package, building all documentation, installs pkgdown and uses it to build the site. It then commits the build site to a separate gh-pages site.
Commit the file and push it up to GitHub to activate. Once the workflow completes successfully, you will find the html for your documentation for you site in a new gh-pages branch!
Letβs commit our current files and changes.
Enable gh-pages
To serve our new site, we need to enable GitHub Pages and set the source directory to the root of the gh-pages branch.