Best practices to alert users of package vignettes when `library(packagename)` is loaded? [closed]
Asked Answered
F

2

8

I'm writing an R package and spend lots of time on the documentation vignettes. I'm wondering if i should add something like

.onAttach <- function( libname , pkgname ){
   packageStartupMessage("use `browseVignettes('packagename')` to see vignettes")
}

that will show up immediately when a user calls

library(packagename)

What is the best way to inform users about package vignettes? Or is it just assumed that users will look for them without the explicit notification?

Fairy answered 28/5, 2016 at 4:4 Comment(4)
long-time R users will probably find this annoying. A novice might have a different view, so I guess it really depends on the package, its purpose and target audience.Stigmatism
@Stigmatism i agree. i wonder if it's smart/possible to print something for maybe only the first ten library(packagename) loads?Fairy
Updated my answer to include a way to "count" package loads.Marinelli
i like the package startup strategy that data.table uses github.com/Rdatatable/data.table/blob/…Fairy
M
7

Ubiquity of Vignettes

Few things to keep in mind about vignettes in the eyes of an everyday R user...

  1. Available on the CRAN package page (it's a great quality check on the package if the authors have one!)
  2. Added to the package binaries and show up on the vignette help section via browseVignettes(package = "pkgname") [as you have pointed out]
  3. Embedded within the R package source
  4. Possibility to use embedded code in vignette as a separate script demo in /demos via demo()
  5. The most important part of all: None of the users really know about vignettes at all.

Thus, if you spend a considerable amount of time on the package documentation, you probably should at least indicate on startup such a feature exists.

Exploiting packageStartupMessage() within .onAttach() for the users benefit.

On package load, the console is relatively clear and the user will definitely see the red text as the contrast is relatively high between normal blue R text (assuming no crayon). However, there are a few cases where it does not make sense to alert the user to the presence of vignettes.

  1. If the session is non-interactive (e.g. bash script).
  2. If the user is an R 'pro'
  3. If the user has been using your package for a long, long, long time leading the user to disregard messages.

Thus, there is negative to a poorly implemented packageStartupMessage().

As a result, I would recommend introducing the startup message under four different conditions.

  1. (All) Check to see if a human is present by querying interactive(). If so, then proceed:
  2. (Standard) Have a list of at least 3 different startup messages and randomly select one per startup to display;
  3. (Normal) Randomly generate a number to determine if a start up message should be displayed;
  4. (Pro) Count package loads, after x package loads kill the startup message until the user reinstalls the package.

I'll present two solutions next that follows the tenet of (1) - (3) and then just (1) and (4) for simplicity sake.

Randomly Selecting Package Hints

Within this version we seek to simply check for humans, provide a break from adding a package load message, and randomly picking one hint to display. Only one dependency is needed outside of the scope of base, the stats::runif command to generate a probability between [0,1].

#' @importFrom stats runif
.onAttach <- function(...) {

  # If interactive, hide message
  # o.w. check against rng seed.
  if (!interactive() || stats::runif(1) > 0.5){
     return()
  }

  # Create a list of helpful tips
  pkg_hints = c(
    "Check for updates and report bugs at https://cran.r-project.org/web/packages/pkgname/.",
    "Use `suppressPackageStartupMessages()` to remove package startup messages.",
    "To see the user guides use `browseVignettes('pkgname')`"
  )

  # Randomly pick one hint
  startup_hint = sample(pkg_hints, 1)

  # Display hint
  packageStartupMessage(paste(strwrap(startup_hint), collapse = "\n"))
}

Counting Package Loads

To use a counter, we make use of the ability to save into the package's install directory and not the "working" directory / user space. This has a few problems associated with it that I'll briefly mention:

  1. The users might only have the rights to view but not modify the system library (user libraries are okay).
    • Thus, they will always receive package hints under this method as they cannot increment the counter.
  2. The counter might be shared if the package is in a system library.
    • This would cause all the hints to be used quickly.
  3. Error logs may increase in size.

Of course, you could create your own folder within the user space via R_USER or HOME environment variables to negate these problems. (Exercise left to the reader, hint: Use Sys.getenv() and dir.create().)

Regardless, one of the beauties of this function is at a later time you could probably include a "send package usage statistics" function within the package. This would actual give a reasonably accurate - phone home - statistic vs. the current RStudio CRAN mirror package download info. But, I digress.

For this method to work, we need to do a bit more prep work on the initial package supplied to CRAN. Specifically, we should probably pre-setup a counter via:

# Set working directory to package
setwd("package_dir")

# Create the inst directory if it does not exist
if(!dir.exists("inst")){
  dir.create("inst")
}

# Create a counter variable
pkg_count = 0

# Save it into a .rda file
save(pkg_count, file="inst/counter.rda")

And now, onto the .onAttach() counter implementation!

.onAttach <- function(...){
  if (!interactive()) return()

  # Get the install directory location of the counter
  path_count = system.file("counter.rda", package = "pkgname")

  # Suppress messages by default
  displayMsg = FALSE

  # Are we able to load the counter?
  a = tryCatch(load(path_count), error = function(e){"error"}, warning = function(e){"warning"})

  # Set error variable
  count_error = a %in% c("error","warning")

  # Check if the count is loaded
  if(!count_error){

    # Check if load count is low... 
    if(pkg_count < 10){
      pkg_count = pkg_count + 1

      # Try to save out
      try(save(pkg_count, file = path_count), silent=TRUE)
      displayMsg = T
    }
  }

  # Display message (can be suppressed with suppressPackageStartupMessages)
  if(displayMsg){
    packageStartupMessage("use `browseVignettes('packagename')` to see vignettes")
  }
}

A last thought

Please keep in mind that if you have any package dependencies (e.g. Depend: in DESCRIPTION), they may have their own set of startup messages that will be displayed before the ones written in your package.

Marinelli answered 28/5, 2016 at 4:33 Comment(14)
You need to be a bit careful about writing to disk in this way - the user might not have write acces to that directory and I think it's against the CRAN policies.Tirrell
@Tirrell Right you are regarding the write to disk aspect. After looking through CRAN's policy, there is a line at the end stating it may be considered malicious or anti-social. Though, this approach does pass the R CMD check --as-cran. Nonetheless, there are considerably more the caveats associated with the counter implementation that I've only included per a request by the original questioner. If I have time, I'll probably substitute in a "read" package info or detect new version release inplace of the counter.Marinelli
hi, it might be smart to verify whether the user explicitly typed library(packagename) as opposed to onAttach triggering due to a dependency by checking that it happened with sys.call(-1) and/or parent.frame?Fairy
@Coatless I think this is pretty clear: "Packages should not write in the users’ home filespace, nor anywhere else on the file system apart from the R session’s temporary directory (or during installation in the location pointed to by TMPDIR: and such usage should be cleaned up)."Tirrell
@Tirrell I agree. However, it's the technicality that comes after it that caused me to say may: "Limited exceptions may be allowed in interactive sessions if the package obtains confirmation from the user." The counter example could be approved if on the first run a user is prompted for approval for a "decaying" hint structure with the option saved in options(). Anyway, as I said before, the inclusion of the counter was more or less to satisfy the initial posters comment. I'm more of a fan of the first example and doing a check to alert the user if the package is the latest.Marinelli
@AnthonyDamico I'm not sure there really is a way to detect this due to the way package loads occur via namespaceImport(). See: github.com/wch/r-source/blob/…Marinelli
@Marinelli would this work? thanks!! github.com/MonetDB/eanthony/issues/24#issuecomment-222473890Fairy
@AnthonyDamico I don't think so given: Warning messages: 1: In substitute(library(anRpackage)) == sys.calls()[[1]] : longer object length is not a multiple of shorter object length 2: In substitute(require(anRpackage)) == sys.calls()[[1]] : longer object length is not a multiple of shorter object lengthMarinelli
@Coatless sorry, how did you trigger that?Fairy
@AnthonyDamico this really should be its own question instead of a line of comments.Marinelli
@Coatless i don't disagree but i'm not really sure what question to ask here :)Fairy
@AnthonyDamico trying to suppress startup messages from a dependent package? There is a previous suggestion given here: #6280308Marinelli
@Coatless sorry, i don't follow? that thread discusses how package authors might suppress startup messages of dependencies. i am asking how to keep silent when my package is the dependency. i'm still confused how you got that longer object length is not a multiple of.. warningFairy
@AnthonyDamico create two packages testpkg and yourpkg. In yourpkg place the code in the linked GitHub issue with a startup message. Build the package. Add a dependency link in testpkg via Depends: in DESCIPTION to yourpkg and add import(yourpkg) in NAMESPACE. Build the testpkg and load in a clean R session.Marinelli
R
6

I'll offer up the contrary opinion (are opinions allowed on StackOverflow?) that startup messages advertising vignettes are not helpful. It is increasingly common for packages to have extensive dependencies, and then the message appears out of context (why am I getting guidance on package Foo when I'm attaching Bar?), is buried in the startup messages of other packages, is confused with a warning or error, or overwhelms potentially informative messages (e.g., about masked symbols) generated in other parts of the process. Also, Bioconductor package vignettes are very common and users quickly learn their value (also the value of some of the amazing vignettes available in non-Bioconductor packages), so users may not be as unfamiliar with the existence and value of vignettes as portrayed.

Each line of code offers the opportunity for bugs, so writing complex code to conditionally display information about vignettes is not a valuable use of programmer time or conducive to seamless user experience, especially when successful execution of the code is required for use of the package! In @Coatless' solutions and only to illustrate this point, in addition to concern about writing to the installed package location, I would also quibble about the use of T rather than TRUE, load() / save() rather than readRDS() / saveRDS(), the non-specific way in which errors or warnings are caught but not communicated to the user, introduction of additional package dependencies (via stats::runif()), and maybe the use of markdown notation in the startup message.

Instead, reference the vignette in the Description: field of the DESCRIPTION file, and on individual help pages.

Resinate answered 28/5, 2016 at 21:19 Comment(2)
As long as you do it onAttach and onLoad you solve the dependency problemTirrell
The comment on messages "advertising vignettes" brought to mind this blog post by @DavidRobinsonBuxtehude

© 2022 - 2024 — McMap. All rights reserved.