4

Background

I am documenting an R package, and I wish the @examples section(s) to include the (commented) output below the code, so the user need not run the examples (in the console, etc.) in order to see the output.

As such, I have typed the output (#> ...) by hand...

#' @name my_topic
#' 
#' @title My Topic
#' @description A page for the general topic, under which the specific function(s) will appear.
#' @export
#' 
#' @examples
#'   # Demo variables available throughout the topic.
#'   unnamed_vec <- 1:3
#'   unnamed_vec
#'   #> [1] 1 2 3
#'   
#'   named_vec <- c(a = 1, b = 2, c = 3)
#'   named_vec
#'   #> a b c
#'   #> 1 2 3



#' @rdname my_topic
#' @export
#' 
#' @param x An \R object.
#' 
#' @details `my_fun()` is an alias for [base::names()].
#' 
#' @return For `my_fun()`, a `character` vector with the names, or `NULL` if no names are found.
#' 
#' @examples
#'   
#'   
#'   
#'   # An application of `my_fun()` specifically.
#'   my_fun(unnamed_vec)
#'   #> NULL
#'   
#'   my_fun(named_vec)
#'   #> [1] "a" "b" "c"
my_fun <- function(x) {
  base::names(x)
}

...in order to yield the following page in the manual:

My Topic

Description

A page for the general topic, under which the specific function(s) will appear.

Usage

my_fun(x)

Arguments

x An R object.

Details

my_fun() is an alias for base::names().

Value

For my_fun(), a character vector with the names, or NULL if no names are found.

Examples
# Demo variables available throughout the topic.
unnamed_vec <- 1:3
unnamed_vec
#> [1] 1 2 3

named_vec <- c(a = 1, b = 2, c = 3)
named_vec
#> a b c
#> 1 2 3



# An application of `my_fun()` specifically.
my_fun(unnamed_vec)
#> NULL

my_fun(named_vec)
#> [1] "a" "b" "c"

Goal

Instead of typing this output (#> ...) by hand, I want it to autogenerate from the executable code, so changes to the commands (my_fun(...)) are dynamically reflected in the output.

I hope to outsource this to R markdown, as with code chunks suggested by roxygen2:

Code chunks

You can insert executable code with ```{r}, just like in knitr documents. For example:

#' @title Title
#' @details Details
#' ```{r lorem}
#' 1+1
#' ```
#' @md
foo <- function() NULL

Note

I see the website for the purrr package seems to autogenerate output for its Examples

Examples

# Reducing `+` computes the sum of a vector while reducing `*`
# computes the product:
1:3 |> reduce(`+`)
#> [1] 6
1:10 |> reduce(`*`)
#> [1] 3628800

from the executable code under its @examples here:

#' @examples
#' # Reducing `+` computes the sum of a vector while reducing `*`
#' # computes the product:
#' 1:3 |> reduce(`+`)
#' 1:10 |> reduce(`*`)

However, this output does not appear on the corresponding Help page in RStudio:

Examples
# Reducing `+` computes the sum of a vector while reducing `*`
# computes the product:
1:3 |> reduce(`+`)
1:10 |> reduce(`*`)

Problem

Unfortunately, when I try to implement code chunks under @examples...

#' @examples
#' ```{r}
#' # Demo variables available throughout the topic.
#' unnamed_vec <- 1:3
#' unnamed_vec
#' 
#' named_vec <- c(a = 1, b = 2, c = 3)
#' named_vec
#' ```
#' @examples
#' ```{r}
#' 
#' 
#' # An application of `my_fun()` specifically.
#' my_fun(unnamed_vec)
#' 
#' my_fun(named_vec)
#' ```

...I do not get the intended result (below) for the manual:

Examples
# Demo variables available throughout the topic.
unnamed_vec <- 1:3
unnamed_vec
#> [1] 1 2 3

named_vec <- c(a = 1, b = 2, c = 3)
named_vec
#> a b c
#> 1 2 3



# An application of `my_fun()` specifically.
my_fun(unnamed_vec)
#> NULL

my_fun(named_vec)
#> [1] "a" "b" "c"

Instead, my check() triggers the following error:

── R CMD check results ─────────────────────────────────────────────────────────────── pkg 0.0.0.9000 ────
Duration: 18.5s

❯ checking examples ... ERROR
  Running examples in ‘my_topic-Ex.R’ failed
  The error most likely occurred in:
  
  > base::assign(".ptime", proc.time(), pos = "CheckExEnv")
  > ### Name: my_topic
  > ### Title: My Topic
  > ### Aliases: my_topic my_fun
  > 
  > ### ** Examples
  > 
  >   ```{r}
  Error: attempt to use zero-length variable name
  Execution halted

❯ checking for unstated dependencies in examples ... WARNING
  Warning: parse error in file 'lines':
  attempt to use zero-length variable name

1 error ✖ | 1 warning ✖ | 0 notes ✔

Note

I have tried to force markdown with the @md tag...

#' @md
#' @examples

...but the error still occurs.

Unfortunately, I suspect that everything under the @examples tag is automatically treated and executed as pure R code, and never as R markdown. So the result is not the .Rd below...

 \examples{
 Examples
 
 if{html}{\out{<div class="sourceCode r">}}\preformatted{
 # Demo variables available throughout the topic.
 unnamed_vec <- 1:3
 unnamed_vec
 #> [1] 1 2 3
 
 named_vec <- c(a = 1, b = 2, c = 3)
 named_vec
 #> a b c
 #> 1 2 3
 
 
 
 # An application of `my_fun()` specifically.
 my_fun(unnamed_vec)
 #> NULL
 
 my_fun(named_vec)
 #> [1] "a" "b" "c"
 }\if{html}{\out{</div>}}
 }

...but rather an attempt to execute this code in R itself...

 ```{r}
 # Demo variables available throughout the topic.
 unnamed_vec <- 1:3
 unnamed_vec
 
 named_vec <- c(a = 1, b = 2, c = 3)
 named_vec
 ```
 ```{r}
 
 
 
 # An application of `my_fun()` specifically.
 my_fun(unnamed_vec)
 
 my_fun(named_vec)
 ```

...before rendering (something like) the .Rd below:

 \examples{
 Examples
 
 \preformatted{
 ```{r}
 # Demo variables available throughout the topic.
 unnamed_vec <- 1:3
 unnamed_vec
 
 named_vec <- c(a = 1, b = 2, c = 3)
 named_vec
 ```
 ```{r}
 
 
 
 # An application of `my_fun()` specifically.
 my_fun(unnamed_vec)
 
 my_fun(named_vec)
 ```
 }
 }

Naturally, trying to execute the first line in R...

 ```{r}

...will yield an error, since the symbol ``` represents a "zero-length variable name":

  > ### ** Examples
  > 
  >   ```{r}
  Error: attempt to use zero-length variable name
  Execution halted

Question

What is the canonical way to autogenerate console output for the executable code in @examples?

I would like to avoid hacking together a fake Examples section like so:

#' @md
#' @section Examples:
#' ```{r}
#' # Demo variables available throughout the topic.
#' unnamed_vec <- 1:3
#' unnamed_vec
#' 
#' named_vec <- c(a = 1, b = 2, c = 3)
#' named_vec
#' ```
Greg
  • 3,054
  • 6
  • 27
  • `pkgdown::build_reference` is probably a good place to start, but I'm not convinced that there is a way to inject the output into the corresponding Rd files. You might have to do that part of the text processing yourself. – Mikael Jagan Mar 26 '23 at 21:26

1 Answers1

1

You can roll your own with parse_Rd and Rd2ex from tools. I've written a rough version here, where rdfile and con are paths or text mode connections to Rd files (the input and output) and comment is the prefix for all output lines inside of \examples{}.

Notably, I haven't tried to divert messages or warnings along with example output, but that can in principle be done by establishing suitable calling handlers before evaluating example code.

withExOutput <- function(rdfile, con = stdout(), comment = "## ") {
    ## Parse Rd file
    rd <- tools::parse_Rd(rdfile)

    ## Write example code to *.R
    exfile <- tempfile()
    on.exit(unlink(exfile))
    tools::Rd2ex(rd, out = exfile)

    ## Determine number of lines in 'Rd2ex' header
    exfile.con <- file(exfile, open = "r")
    skip <- 1L
    while (length(zzz <- readLines(exfile.con, n = 1L)) &&
           !length(grep("^### \\*\\*", zzz)))
        skip <- skip + 1L
    while (length(zzz <- readLines(exfile.con, n = 1L)) &&
           !nzchar(zzz))
        skip <- skip + 1L
    close(exfile.con)

    ## Run examples without header and capture stdout/stderr
    x <- capture.output(source(
        exfile, local = new.env(parent = .GlobalEnv),
        echo = TRUE, spaced = FALSE, verbose = FALSE,
        prompt.echo = "~~~~> ", continue.echo = "~~~~+ ",
        skip.echo = skip))

    ## Strip source prompts, comment output lines
    prompted <- grep("^~~~~[>+] ", x)
    x[ prompted] <- sub("^~~~~[>+] ", "", x[prompted])
    x[-prompted] <- paste0(comment, x[-prompted])

    ## Replace \dontrun{}, \dontshow{}, \donttest{}
    s1 <- c("## Not run: ",    "## End(Not run)",
            "## Don't show: ", "## End(Don't show)",
            "## No test: ",    "## End(No test)")
    s2 <- c("\\dontrun{",      "}",
            "\\dontshow{",     "}",
            "\\donttest{",     "}")
    mx <- match(x, s1, 0L)
    x[mx > 0L] <- s2[mx]

    ## Uncomment lines inside of \dontrun{}
    x <- sub("^##D ", "", x)

    ## Delete trailing empty lines
    n <- length(x)
    while (n && !nzchar(x[n]))
        n <- n - 1L
    x <- x[seq_len(n)]

    ## Replace old \examples{} with new {including output}
    rdfile <- tempfile()
    on.exit(unlink(rdfile), add = TRUE)
    writeLines(c("\\examples{", x, "}"), rdfile)
    tags <- vapply(rd, attr, "", "Rd_tag")
    rd[tags == "\\examples"] <- tools::parse_Rd(rdfile)[1L]

    cat(as.character(rd), file = con, sep = "")
}

Taking any Rd file as an example:

%% this is add.Rd
\name{add}
\alias{add}
\title{Add two numbers}
\description{Computes \code{a + b} given \code{a} and \code{b}.}
\usage{
add(a, b)
}
\arguments{
  \item{a}{A number.}
  \item{b}{Another number.}
}
\examples{
(a <- 0)
add(a, 1)
\dontrun{
add(a, 2)
}
\dontshow{
add(a, 3)
}
\donttest{
add(a, 4)
}
}

You would attach your package with library, then call withExOutput with suitable arguments (e.g., probably ensuring that comment starts with a #):

add <- function(a, b) a + b # simulating a 'library' call
withExOutput("add.Rd", comment = "#> ") # in the style of 'reprex'

When you accept the default con = stdout(), the "new" Rd file with example output is just printed in your console (assuming that you haven't diverted it somewhere else):

%% this is add.Rd
\name{add}
\alias{add}
\title{Add two numbers}
\description{Computes \code{a + b} given \code{a} and \code{b}.}
\usage{
add(a, b)
}
\arguments{
  \item{a}{A number.}
  \item{b}{Another number.}
}
\examples{
(a <- 0)
#> [1] 0
add(a, 1)
#> [1] 1
\dontrun{
add(a, 2)
}
\dontshow{
add(a, 3)
#> [1] 3
}
\donttest{
add(a, 4)
#> [1] 4
}
}
Mikael Jagan
  • 9,012
  • 2
  • 17
  • 48
  • Hi Mikael, thank you for your custom solution! I've been too busy to test this, so I haven't yet marked it correct. But yours is the only answer, and I didn't want your hard work to go to waste, so I have awarded the bounty for now. I will comment here with any further updates from its use with my package. :) – Greg Apr 02 '23 at 03:53