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.
potions
has three basic functions:
brew()
to store datapour()
to retrieve datadrain()
to clear dataThe first step is to store data using brew()
, which
accepts data in three formats:
brew(x = 1)
list
, e.g. brew(list(x = 1))
brew(file = "my-config-file.yml")
Information stored using brew
can be retrieved using
pour
:
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).
config
filesOften 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.
You can then simply add the corresponding file name to your
gitignore
, and your script will still run, without sharing
sensitive information.
potions
in package developmentWhen weighing up architectural decisions about how packages should share information between functions, there are a few solutions that developers can choose between:
sysdata.rda
, which supports internal use of named objects
while avoiding options()
completely.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).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()
: