diff --git a/NAMESPACE b/NAMESPACE index 89b84f82..5a9548af 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,6 +14,7 @@ export(bulletize) export(cred_funs_add) export(cred_funs_clear) export(cred_funs_list) +export(cred_funs_list_default) export(cred_funs_set) export(cred_funs_set_default) export(credentials_app_default) @@ -37,6 +38,7 @@ export(gargle_oauth_sitrep) export(gargle_oob_default) export(gargle_verbosity) export(init_AuthState) +export(local_cred_funs) export(local_gargle_verbosity) export(oauth_app_from_json) export(oauth_external_token) @@ -53,6 +55,7 @@ export(token_email) export(token_fetch) export(token_tokeninfo) export(token_userinfo) +export(with_cred_funs) export(with_gargle_verbosity) import(fs) import(rlang) diff --git a/NEWS.md b/NEWS.md index f0326762..4ae0096c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -53,19 +53,28 @@ This is especially likely to come up with gmailr / the Gmail API. * The credential-fetcher `credentials_byo_oauth2()` has been moved to the very beginning of the default registry. The logic is that a user who has specified a non-`NULL` value of `token` must mean business and does not want automagic - auth methods like ADC or GCE to be tried before using their `token` (#187). + auth methods like ADC or GCE to be tried before using their `token` + (#187, #225). * The `...` in `cred_funs_all()` are now - [dynamic dots](https://rlang.r-lib.org/reference/dyn-dots.html). + [dynamic dots](https://rlang.r-lib.org/reference/dyn-dots.html) (#224). * Every registered credential function must have a unique name now. - This is newly enforced by `cred_funs_add()` and `cred_funs_set()`. + This is newly enforced by `cred_funs_add()` and `cred_funs_set()` (#224). + +* `cred_funs_list_default()` is a new function that returns gargle's default + list of credential functions (#226). * `cred_funs_add(cred_fun = NULL)` is now available to remove a credential - function from the registry. + function from the registry (#224). + +* `with_cred_funs()` and `local_cred_funs()` are new helpers for making narrowly + scoped changes to the registry (#226). + +* The `ls` argument of `cred_funs_set()` has been renamed to `funs` (#226). * In general, credential registry functions now return the current registry, - invisibly. + invisibly (#224). # gargle 1.2.1 diff --git a/R/credential-function-registry.R b/R/credential-function-registry.R index e37ac68a..654486ae 100644 --- a/R/credential-function-registry.R +++ b/R/credential-function-registry.R @@ -32,6 +32,25 @@ #' #' # restore the default list #' cred_funs_set_default() +#' +#' # run some code with a temporary change to the registry +#' # creds_one ONLY +#' with_cred_funs( +#' list(one = creds_one), +#' names(cred_funs_list()) +#' ) +#' # add creds_one to the list +#' with_cred_funs( +#' list(one = creds_one), +#' names(cred_funs_list()), +#' action = "modify" +#' ) +#' # remove credentials_gce +#' with_cred_funs( +#' list(credentials_gce = NULL), +#' names(cred_funs_list()), +#' action = "modify" +#' ) NULL #' @describeIn cred_funs Get the list of registered credential functions. @@ -45,7 +64,6 @@ cred_funs_list <- function() { #' * "First registered, last tried." #' * "Last registered, first tried." #' - #' @param ... <[`dynamic-dots`][rlang::dyn-dots]> One or more credential #' functions, in `name = value` form. Each credential function is subject to a #' superficial check that it at least "smells like" a credential function: its @@ -91,11 +109,22 @@ cred_funs_add <- function(...) { #' @describeIn cred_funs Register a list of credential fetching functions. #' -#' @param ls A named list of credential functions. +#' @param funs A named list of credential functions. +#' @param ls `r lifecycle::badge("deprecated")` This argument has been renamed +#' to `funs`. #' @export -cred_funs_set <- function(ls) { - cred_funs_check(ls, allow_null = FALSE) - gargle_env$cred_funs <- ls +cred_funs_set <- function(funs, ls = deprecated()) { + if (lifecycle::is_present(ls)) { + lifecycle::deprecate_warn( + when = "1.3.0", + what = "cred_funs_set(ls)", + with = "cred_funs_set(funs)", + ) + funs = ls + } + + cred_funs_check(funs, allow_null = FALSE) + gargle_env$cred_funs <- funs invisible(cred_funs_list()) } @@ -106,11 +135,10 @@ cred_funs_clear <- function() { invisible(cred_funs_list()) } -#' @describeIn cred_funs Reset the registry to the gargle default. +#' @describeIn cred_funs Return the default list of credential functions. #' @export -cred_funs_set_default <- function() { - cred_funs_clear() - l <- list( +cred_funs_list_default <- function() { + list( credentials_byo_oauth2 = credentials_byo_oauth2, credentials_service_account = credentials_service_account, credentials_external_account = credentials_external_account, @@ -118,7 +146,48 @@ cred_funs_set_default <- function() { credentials_gce = credentials_gce, credentials_user_oauth2 = credentials_user_oauth2 ) - cred_funs_set(l) +} + +#' @describeIn cred_funs Reset the registry to the gargle default. +#' @export +cred_funs_set_default <- function() { + cred_funs_set(cred_funs_list_default()) +} + +#' @describeIn cred_funs Modify the credential function registry in the current +#' scope. It is an example of the `local_*()` functions in \pkg{withr}. +#' @param action Whether to use `funs` to replace or modify the registry with +#' funs: +#' * `"replace"` does `cred_funs_set(funs)` +#' * `"modify"` does `cred_funs_add(!!!funs)` +#' @param .local_envir The environment to use for scoping. Defaults to current +#' execution environment. +#' @export +local_cred_funs <- function(funs = cred_funs_list_default(), + action = c("replace", "modify"), + .local_envir = parent.frame()) { + action <- arg_match(action) + + cred_funs_orig <- cred_funs_list() + withr::defer(cred_funs_set(cred_funs_orig), envir = .local_envir) + + switch( + action, + replace = cred_funs_set(funs), + modify = cred_funs_add(!!!funs) + ) +} + +#' @describeIn cred_funs Evaluate `code` with a temporarily modified credential +#' function registry. It is an example of the `with_*()` functions in +#' \pkg{withr}. +#' @param code Code to run with temporary active project. +#' @export +with_cred_funs <- function(funs = cred_funs_list_default(), + code, + action = c("replace", "modify")) { + local_cred_funs(funs = funs, action = action) + force(code) } cred_funs_check <- function(ls, allow_null = FALSE) { diff --git a/man/cred_funs.Rd b/man/cred_funs.Rd index 34abd777..0eedc9fa 100644 --- a/man/cred_funs.Rd +++ b/man/cred_funs.Rd @@ -6,18 +6,35 @@ \alias{cred_funs_add} \alias{cred_funs_set} \alias{cred_funs_clear} +\alias{cred_funs_list_default} \alias{cred_funs_set_default} +\alias{local_cred_funs} +\alias{with_cred_funs} \title{Credential function registry} \usage{ cred_funs_list() cred_funs_add(...) -cred_funs_set(ls) +cred_funs_set(funs, ls = deprecated()) cred_funs_clear() +cred_funs_list_default() + cred_funs_set_default() + +local_cred_funs( + funs = cred_funs_list_default(), + action = c("replace", "modify"), + .local_envir = parent.frame() +) + +with_cred_funs( + funs = cred_funs_list_default(), + code, + action = c("replace", "modify") +) } \arguments{ \item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> One or more credential @@ -27,7 +44,22 @@ first argument must be named \code{scopes}, and its signature must include \code{...}. To remove a credential function, you can use a specification like \code{name = NULL}.} -\item{ls}{A named list of credential functions.} +\item{funs}{A named list of credential functions.} + +\item{ls}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} This argument has been renamed +to \code{funs}.} + +\item{action}{Whether to use \code{funs} to replace or modify the registry with +funs: +\itemize{ +\item \code{"replace"} does \code{cred_funs_set(funs)} +\item \code{"modify"} does \code{cred_funs_add(!!!funs)} +}} + +\item{.local_envir}{The environment to use for scoping. Defaults to current +execution environment.} + +\item{code}{Code to run with temporary active project.} } \value{ A list of credential functions or \code{NULL}. @@ -51,8 +83,17 @@ Function(s) are added to the \emph{front} of the list. So: \item \code{cred_funs_clear()}: Clear the credential function registry. +\item \code{cred_funs_list_default()}: Return the default list of credential functions. + \item \code{cred_funs_set_default()}: Reset the registry to the gargle default. +\item \code{local_cred_funs()}: Modify the credential function registry in the current +scope. It is an example of the \verb{local_*()} functions in \pkg{withr}. + +\item \code{with_cred_funs()}: Evaluate \code{code} with a temporarily modified credential +function registry. It is an example of the \verb{with_*()} functions in +\pkg{withr}. + }} \examples{ names(cred_funs_list()) @@ -79,6 +120,25 @@ names(cred_funs_list()) # restore the default list cred_funs_set_default() + +# run some code with a temporary change to the registry +# creds_one ONLY +with_cred_funs( + list(one = creds_one), + names(cred_funs_list()) +) +# add creds_one to the list +with_cred_funs( + list(one = creds_one), + names(cred_funs_list()), + action = "modify" +) +# remove credentials_gce +with_cred_funs( + list(credentials_gce = NULL), + names(cred_funs_list()), + action = "modify" +) } \seealso{ \code{\link[=token_fetch]{token_fetch()}}, which is where the registry is actually used. diff --git a/tests/testthat/_snaps/credential-function-registry.md b/tests/testthat/_snaps/credential-function-registry.md index b676ee89..a9ddee91 100644 --- a/tests/testthat/_snaps/credential-function-registry.md +++ b/tests/testthat/_snaps/credential-function-registry.md @@ -31,3 +31,12 @@ Error in `cred_funs_check()`: ! Each credential function must have a unique name +# cred_funs_set() warns for use of `ls` + + Code + out <- cred_funs_set(ls = list(a = function(scopes, ...) { })) + Condition + Warning: + The `ls` argument of `cred_funs_set()` is deprecated as of gargle 1.3.0. + i Please use the `funs` argument instead. + diff --git a/tests/testthat/test-credential-function-registry.R b/tests/testthat/test-credential-function-registry.R index 8761f399..1b036750 100644 --- a/tests/testthat/test-credential-function-registry.R +++ b/tests/testthat/test-credential-function-registry.R @@ -1,4 +1,4 @@ -test_that("We recognize a credential function signature", { +test_that("is_cred_fun() recognizes a credential function signature", { creds_one <- function(scopes, ...) {} creds_two <- function(scopes, arg1, arg2 = "optional", ...) {} expect_true(is_cred_fun(creds_one)) @@ -12,7 +12,7 @@ test_that("We recognize a credential function signature", { expect_false(is_cred_fun(invalid_three)) }) -test_that("We can register new credential functions", { +test_that("cred_funs_add() and _set() can add and set credential functions", { creds_one <- function(scopes, ...) {} creds_two <- function(scopes, arg1, arg2 = "optional", ...) {} @@ -39,6 +39,22 @@ test_that("We can register new credential functions", { expect_equal(names(cred_funs_list()), c("first", "last")) }) +test_that("We insist on valid credential function (or NULL)", { + creds_one <- function(scopes, ...) {} + + withr::defer(cred_funs_set_default()) + cred_funs_clear() + + expect_snapshot( + error = TRUE, + cred_funs_add(a = mean) + ) + expect_snapshot( + error = TRUE, + cred_funs_set(list(a = NULL)) + ) +}) + test_that("We insist on uniquely named credential functions", { creds_one <- function(scopes, ...) {} @@ -87,3 +103,51 @@ test_that("We can remove credential functions by name", { cred_funs_add(c = NULL, d = cred_fun) expect_equal(names(cred_funs_list()), c("d", "a")) }) + +test_that("cred_funs_set() warns for use of `ls`", { + withr::local_options(lifecycle_verbosity = "warning") + withr::defer(cred_funs_set_default()) + expect_snapshot( + out <- cred_funs_set(ls = list(a = function(scopes, ...) {})) + ) + expect_equal(names(cred_funs_list()), "a") +}) + +test_that("local_cred_funs() works", { + withr::defer(cred_funs_set_default()) + cred_fun <- function(scopes, ...) {} + cred_funs_clear() + + cred_funs_add(a = cred_fun, b = cred_fun) + + local_cred_funs(funs = list(c = cred_fun), action = "modify") + expect_equal(names(cred_funs_list()), c("c", "b", "a")) + + local_cred_funs(funs = list(d = cred_fun)) # action = "replace" + expect_equal(names(cred_funs_list()), "d") + + local_cred_funs() + expect_equal(names(cred_funs_list()), names(cred_funs_list_default())) +}) + +test_that("with_cred_funs() works", { + withr::defer(cred_funs_set_default()) + cred_fun <- function(scopes, ...) {} + cred_funs_clear() + + cred_funs_add(a = cred_fun, b = cred_fun) + + with_cred_funs( + funs = list(c = cred_fun), action = "modify", + expect_equal(names(cred_funs_list()), c("c", "b", "a")) + ) + + with_cred_funs( + funs = list(d = cred_fun), # action = "replace" + expect_equal(names(cred_funs_list()), "d") + ) + + with_cred_funs( + code = expect_equal(names(cred_funs_list()), names(cred_funs_list_default())) + ) +})