Skip to contents

potions is a package for easily storing and retrieving information via options(). It therefore provides functionality somewhat similar to {settings}, but with syntax based more closely on {here}. The intended use of potions is for adding novel information to options() for use within single packages or workflows.

Basic usage

potions has three basic functions:

The first step is to store data using brew(), which accepts data in three formats:

  • Named arguments, e.g. brew(x = 1)
  • A list, e.g. brew(list(x = 1))
  • A configuration file, e.g. brew(file = "my-config-file.yml")

Information stored using brew can be retrieved using pour:

library(potions)

brew(x = 1)

paste0("The value of x is ", pour("x"))
#> [1] "The value of x is 1"

drain()

Interactions with global options

Because potions uses a novel S3 object for all data storage, it never overwrites existing global options, and is therefore safe to use without affecting existing workflows. For example, print.default takes it’s default digits argument from getOption("digits"):

options("digits") # set to 7 by default
#> $digits
#> [1] 7
print(pi)
#> [1] 3.141593

If we use potions to set digits, we do not affect this behaviour. Instead, the user must specifically retrieve data using pour for these settings to be applied:

library(potions)
brew(digits = 3)

print(pi, digits = pour("digits")) # using potions
#> [1] 3.14
print(pi) # default is unaffected
#> [1] 3.141593

This feature - i.e. storing data in a novel S3 object - means that potions can distinguish between interactive use in the console versus being called within a package. Data can be provided and used independently by multiple packages, and in the console, without generating conflicts.

Options stored using potions are not persistent across sessions; you will need to reload options each time you open a new workspace. It is unlikely, therefore, that you will need to ‘clear’ the data stored by potions at any point. If you do need to remove data, you can do so using drain() (without any further arguments).

Using config files

Often it is necessary to share a script, but without sharing certain sensitive information necessary to run the code. A common example is API keys or other sensitive information required to download data from a web service. In such cases, the default, interactive method of using brew() is insufficient, i.e.

# start of script
brew(list("my-secret-key" = "123456")) # shares secret information

To avoid this problem, you can instead supply the path to a file containing that information, i.e.

brew(file = "config.yml") # hides secret information

You can then simply add the corresponding file name to your gitignore, and your script will still run, without sharing sensitive information.

Using potions in package development

When weighing up architectural decisions about how packages should share information between functions, there are a few solutions that developers can choose between:

  • Where a developer needs to be able to call static information across multiple functions, an efficient solution is to use sysdata.rda, which supports internal use of named objects while avoiding options() completely.
  • Where a function relies on information stored in options(), and for which there is no override, it is possible to temporarily reset options() within a function. In these cases, CRAN requires that the initial state be restored after use, for which on.exit() is a sensible choice (See Advanced R section 6.7.4).
  • Finally, where there is a need for dynamic, package-wide options that can be changed by the developer or the user, packages such as potions or settings can be valuable.

To use potions in a package development situation, create a file in the R directory called onLoad.R, containing the following code:

.onLoad <- function(libname, pkgname) {
  if(pkgname == "packagenamehere") {
    potions::brew(.pkg = "packagenamehere")
  }
}

This is important because it tells potions that you are developing a package, what that package is called, and where future calls to brew() from within that package should place their data. It is also possible to add defaults here, e.g.

.onLoad <- function(libname, pkgname) {
  if(pkgname == "packagenamehere") {
    potions::brew(
      n_attempts == 5,
      verbose == TRUE,
      .pkg = "packagenamehere")
  }
}

Often when developing a package, you will want users to call your own configuration function, rather than call brew() directly. This provides greater control over the names & types of data stored by potions, which in turn gives you - the developer - greater certainty when calling those data within your package via pour(). For example, you might want to specify that a specific argument is supplied as numeric:

packagename_config <- function(fontsize = 10){
  if(!is.numeric(fontsize)){
    rlang::abort("Argument `fontsize` must be a number")
  }
  brew(list(fontsize = fontsize))
}

An additional benefit of writing a wrapper function is to allow users to provide their own config file. The easiest way to do this is to support a file argument within your own function, then pass this directly to brew():

packagename_config <- function(file = NULL){
  if(!is.null(file)){
    brew(file = file)
  }
}

This approach is risky, however, as it doesn’t allow any checks. An alternative is to intercept the file, run your own checks, then pass the result to brew():

packagename_config <- function(file = NULL){
  if(!is.null(file)){
    config_data <- potions::read_config(x)
    # add any checks to `data` that are needed here
    if(length(names(data)) != length(data)){
      rlang::abort("Not all entries are named!")
    }
    # pass to `brew`
    brew(config_data)
  }
}