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
:
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")
:
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.
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 avoidingoptions()
completely. - Where a function relies on information stored in
options()
, and for which there is no override, it is possible to temporarily resetoptions()
within a function. In these cases, CRAN requires that the initial state be restored after use, for whichon.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()
:
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()
: