diff --git a/DESCRIPTION b/DESCRIPTION index fed00772..2325b191 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,7 +19,8 @@ Imports: lifecycle (>= 1.0.3), magrittr (>= 1.5.0), rlang (>= 1.1.1), - vctrs (>= 0.6.3) + vctrs (>= 0.6.3), + S7 (>= 0.2.0) Suggests: covr, dplyr (>= 0.7.8), diff --git a/NAMESPACE b/NAMESPACE index bb906a65..44b42a02 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,10 +6,6 @@ S3method(as_mapper,list) S3method(as_mapper,numeric) S3method(print,purrr_function_compose) S3method(print,purrr_function_partial) -S3method(print,purrr_rate_backoff) -S3method(print,purrr_rate_delay) -S3method(rate_sleep,purrr_rate_backoff) -S3method(rate_sleep,purrr_rate_delay) export("%>%") export("%@%") export("%||%") @@ -200,6 +196,7 @@ export(walk) export(walk2) export(when) export(zap) +import(S7) import(rlang) import(vctrs) importFrom(cli,cli_progress_bar) diff --git a/R/package-purrr.R b/R/package-purrr.R index e3f7d0a9..fdead57b 100644 --- a/R/package-purrr.R +++ b/R/package-purrr.R @@ -1,6 +1,7 @@ #' @keywords internal #' @import rlang #' @import vctrs +#' @import S7 #' @importFrom cli cli_progress_bar #' @importFrom lifecycle deprecated #' @useDynLib purrr, .registration = TRUE diff --git a/R/rate.R b/R/rate.R index 3e6526d4..5200a641 100644 --- a/R/rate.R +++ b/R/rate.R @@ -17,21 +17,66 @@ #' @name rate-helpers NULL +rate <- new_class( + "rate", + package = "purrr", + properties = list( + jitter = new_property(class_logical, + validator = function(value) { + if (!is_bool(value)) { + "must be a logical of length 1" + } + } + ), + max_times = new_property(class_numeric, + validator = function(value) { + if (!is_number(value, allow_infinite = TRUE)) { + "must be a numeric or `Inf`" + } + } + ), state = new_property(class_environment) + ), + constructor = function(jitter = TRUE, max_times = 3, state = env(i = 0L)) { + force(jitter) + force(max_times) + force(state) + + new_object(S7_object(), + jitter = jitter, max_times = max_times, state = state + ) + } +) + + #' @rdname rate-helpers #' @param pause Delay between attempts in seconds. #' @export -rate_delay <- function(pause = 1, - max_times = Inf) { - - check_number_decimal(pause, allow_infinite = TRUE, min = 0) - - new_rate( - "purrr_rate_delay", - pause = pause, - max_times = max_times, - jitter = FALSE - ) -} +rate_delay <- new_class("rate_delay", + parent = rate, + package = "purrr", + properties = list( + pause = new_property(class_numeric, validator = function(value) { + check_number_decimal(value, allow_infinite = TRUE, min = 0) + }), + max_times = new_property(class_numeric, + default = Inf, + validator = function(value) { + if (!is_number(value, allow_infinite = TRUE)) { + "must be a numeric or `Inf`" + } + } + ) + ), + constructor = function(pause = 1, max_times = Inf, jitter = FALSE) { + force(pause) + force(jitter) + force(max_times) + + new_object(rate(jitter = jitter, max_times = max_times), + pause = pause + ) + } +) #' @rdname rate-helpers #' @param pause_base,pause_cap `rate_backoff()` uses an exponential @@ -41,72 +86,63 @@ rate_delay <- function(pause = 1, #' only necessary if you need pauses less than one second (which may #' not be kind to the server, use with caution!). #' @export -rate_backoff <- function(pause_base = 1, - pause_cap = 60, - pause_min = 1, - max_times = 3, - jitter = TRUE) { - - check_number_decimal(pause_base, min = 0) - check_number_decimal(pause_cap, allow_infinite = TRUE, min = 0) - check_number_decimal(pause_min, allow_infinite = TRUE, min = 0) - check_number_whole(max_times, min = 1) - check_bool(jitter) - - new_rate( - "purrr_rate_backoff", - pause_base = pause_base, - pause_cap = pause_cap, - pause_min = pause_min, - max_times = max_times, - jitter = jitter - ) -} - -new_rate <- function(.subclass, ..., jitter = TRUE, max_times = 3) { - stopifnot( - is_bool(jitter), - is_number(max_times) || identical(max_times, Inf) - ) - - rate <- list( - ..., - state = env(i = 0L), - jitter = jitter, - max_times = max_times - ) +rate_backoff <- new_class( + "rate_backoff", + parent = rate, + package = "purrr", + properties = list( + pause_base = new_property(class_numeric, validator = function(value) { + check_number_decimal(value, min = 0) # TODO: maybe allow_infinite needs to be FALSE? + }), + pause_cap = new_property(class_numeric, validator = function(value) { + check_number_decimal(value, allow_infinite = TRUE, min = 0) + }), + pause_min = new_property(class_numeric, validator = function(value) { + check_number_decimal(value, allow_infinite = TRUE, min = 0) + }) + ), + constructor = function(pause_base = 1, pause_cap = 60, pause_min = 1, max_times = 3, jitter = TRUE) { + force(pause_base) + force(pause_cap) + force(pause_min) + force(max_times) + force(jitter) + + new_object(rate(jitter = jitter, max_times = max_times), + pause_base = pause_base, pause_cap = pause_cap, pause_min = pause_min + ) + } +) - structure( - rate, - class = c(.subclass, "purrr_rate") - ) -} #' @rdname rate-helpers #' @param x An object to test. #' @export is_rate <- function(x) { - inherits(x, "purrr_rate") + S7_inherits(x, rate) } + +base_print <- new_external_generic("base", "print", "x") + #' @export -print.purrr_rate_delay <- function(x, ...) { +method(base_print, rate_delay) <- function(x, ...) { cli::cli_text("") cli::cli_bullets(c( - " " = "Attempts: {rate_count(x)}/{x$max_times}", - " " = "{.field pause}: {x$pause}" + " " = "Attempts: {rate_count(x)}/{x@max_times}", + " " = "{.field pause}: {x@pause}" )) invisible(x) } + #' @export -print.purrr_rate_backoff <- function(x, ...) { +method(base_print, rate_backoff) <- function(x, ...) { cli::cli_text("") - cli::cli_bullets(c( - " " = "Attempts: {rate_count(x)}/{x$max_times}", - " " = "{.field pause_base}: {x$pause_base}", - " " = "{.field pause_cap}: {x$pause_cap}", - " " = "{.field pause_min}: {x$pause_min}" + " " = "Attempts: {rate_count(x)}/{x@max_times}", + " " = "{.field pause_base}: {x@pause_base}", + " " = "{.field pause_cap}: {x@pause_cap}", + " " = "{.field pause_min}: {x@pause_min}" )) invisible(x) @@ -127,15 +163,15 @@ print.purrr_rate_backoff <- function(x, ...) { #' @seealso [rate_backoff()], [insistently()] #' @keywords internal #' @export -rate_sleep <- function(rate, quiet = TRUE) { +rate_sleep <- new_generic("rate_sleep", "rate", function(rate, ..., quiet = TRUE) { stopifnot(is_rate(rate)) i <- rate_count(rate) - if (i > rate$max_times) { + if (i > rate@max_times) { stop_rate_expired(rate) } - if (i == rate$max_times) { + if (i == rate@max_times) { stop_rate_excess(rate) } @@ -146,24 +182,24 @@ rate_sleep <- function(rate, quiet = TRUE) { } on.exit(rate_bump_count(rate)) - UseMethod("rate_sleep") -} + S7_dispatch() +}) #' @export -rate_sleep.purrr_rate_backoff <- function(rate, quiet = TRUE) { +method(rate_sleep, rate_backoff) <- function(rate, quiet = TRUE) { i <- rate_count(rate) - pause_max <- min(rate$pause_cap, rate$pause_base * 2^i) - if (rate$jitter) { + pause_max <- min(rate@pause_cap, rate@pause_base * 2^i) + if (rate@jitter) { pause_max <- stats::runif(1, 0, pause_max) } - length <- max(rate$pause_min, pause_max) + length <- max(rate@pause_min, pause_max) rate_sleep_impl(rate, length, quiet) } #' @export -rate_sleep.purrr_rate_delay <- function(rate, quiet = TRUE) { - rate_sleep_impl(rate, rate$pause, quiet) +method(rate_sleep, rate_delay) <- function(rate, quiet = TRUE) { + rate_sleep_impl(rate, rate@pause, quiet) } rate_sleep_impl <- function(rate, length, quiet) { @@ -178,16 +214,16 @@ rate_sleep_impl <- function(rate, length, quiet) { rate_reset <- function(rate) { stopifnot(is_rate(rate)) - rate$state$i <- 0L + rate@state$i <- 0L invisible(rate) } rate_count <- function(rate) { - rate$state$i + rate@state$i } rate_bump_count <- function(rate, n = 1L) { - rate$state$i <- rate$state$i + n + rate@state$i <- rate@state$i + n invisible(rate) } @@ -236,4 +272,3 @@ check_rate <- function(rate, error_call = caller_env()) { ) } } - diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 00000000..d46037e7 --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,3 @@ +.onLoad <- function(lib, pkg) { + S7::methods_register() +} diff --git a/man/rate-helpers.Rd b/man/rate-helpers.Rd index 34645556..3ace6108 100644 --- a/man/rate-helpers.Rd +++ b/man/rate-helpers.Rd @@ -7,7 +7,7 @@ \alias{is_rate} \title{Create delaying rate settings} \usage{ -rate_delay(pause = 1, max_times = Inf) +rate_delay(pause = 1, max_times = Inf, jitter = FALSE) rate_backoff( pause_base = 1, @@ -24,6 +24,8 @@ is_rate(x) \item{max_times}{Maximum number of requests to attempt.} +\item{jitter}{Whether to introduce a random jitter in the waiting time.} + \item{pause_base, pause_cap}{\code{rate_backoff()} uses an exponential back-off so that each request waits \code{pause_base * 2^i} seconds, up to a maximum of \code{pause_cap} seconds.} @@ -32,8 +34,6 @@ up to a maximum of \code{pause_cap} seconds.} only necessary if you need pauses less than one second (which may not be kind to the server, use with caution!).} -\item{jitter}{Whether to introduce a random jitter in the waiting time.} - \item{x}{An object to test.} } \description{ diff --git a/man/rate_sleep.Rd b/man/rate_sleep.Rd index 0c93d914..ce1c278c 100644 --- a/man/rate_sleep.Rd +++ b/man/rate_sleep.Rd @@ -5,7 +5,7 @@ \alias{rate_reset} \title{Wait for a given time} \usage{ -rate_sleep(rate, quiet = TRUE) +rate_sleep(rate, ..., quiet = TRUE) rate_reset(rate) } diff --git a/tests/testthat/_snaps/rate.md b/tests/testthat/_snaps/rate.md index 2ee1ceeb..cf45b16e 100644 --- a/tests/testthat/_snaps/rate.md +++ b/tests/testthat/_snaps/rate.md @@ -18,7 +18,7 @@ # rate_delay() delays Code - rate_sleep(rate) + rate_sleep(rd) Condition Error in `rate_sleep()`: ! Request failed after 3 attempts. @@ -26,7 +26,7 @@ --- Code - rate_sleep(rate) + rate_sleep(rd) Condition Error in `rate_sleep()`: ! This `rate` object has already be run more than `max_times` allows. @@ -35,7 +35,7 @@ # rate_backoff() backs off Code - rate_sleep(rate) + rate_sleep(rb) Condition Error in `rate_sleep()`: ! Request failed after 3 attempts. @@ -43,7 +43,7 @@ --- Code - rate_sleep(rate) + rate_sleep(rb) Condition Error in `rate_sleep()`: ! This `rate` object has already be run more than `max_times` allows. @@ -52,7 +52,7 @@ # rate_sleep() checks that rate is still valid Code - rate_sleep(rate) + rate_sleep(rd) Condition Error in `rate_sleep()`: ! Request failed after 0 attempts. @@ -60,7 +60,7 @@ --- Code - rate_sleep(rate) + rate_sleep(rd) Condition Error in `rate_sleep()`: ! This `rate` object has already be run more than `max_times` allows. diff --git a/tests/testthat/test-rate.R b/tests/testthat/test-rate.R index 1a29c90f..74e92168 100644 --- a/tests/testthat/test-rate.R +++ b/tests/testthat/test-rate.R @@ -1,19 +1,22 @@ test_that("new_rate() creates rate objects", { - rate <- new_rate("foo", jitter = FALSE, max_times = 10) - expect_identical(rate$state$i, 0L) - expect_identical(rate$max_times, 10) - expect_false(rate$jitter) + # rate <- new_rate("foo", jitter = FALSE, max_times = 10) + rate <- rate(jitter = FALSE, max_times = 10) + + expect_identical(rate@state$i, 0L) + expect_identical(rate@max_times, 10) + expect_false(rate@jitter) }) test_that("can bump and reset count", { - rate <- new_rate("foo") + # rate <- new_rate("foo") + r <- rate() - rate_bump_count(rate) - rate_bump_count(rate) - expect_identical(rate_count(rate), 2L) + rate_bump_count(r) + rate_bump_count(r) + expect_identical(rate_count(r), 2L) - rate_reset(rate) - expect_identical(rate_count(rate), 0L) + rate_reset(r) + expect_identical(rate_count(r), 0L) }) test_that("rates have print methods", { @@ -26,53 +29,53 @@ test_that("rates have print methods", { }) test_that("rate_delay() delays", { - rate <- rate_delay( + rd <- rate_delay( pause = 0.02, max_times = 3 ) - rate_sleep(rate, quiet = FALSE) + rate_sleep(rd, quiet = FALSE) - rate_reset(rate) + rate_reset(rd) - msg <- catch_cnd(rate_sleep(rate)) + msg <- catch_cnd(rate_sleep(rd)) expect_true(inherits_all(msg, c("purrr_condition_rate_init", "condition"))) - msg <- catch_cnd(rate_sleep(rate, quiet = FALSE)) + msg <- catch_cnd(rate_sleep(rd, quiet = FALSE)) expect_true(inherits_all(msg, c("purrr_message_rate_retry", "message"))) expect_identical(msg$length, 0.02) - msg <- catch_cnd(rate_sleep(rate, quiet = FALSE)) + msg <- catch_cnd(rate_sleep(rd, quiet = FALSE)) expect_identical(msg$length, 0.02) - expect_snapshot(rate_sleep(rate), error = TRUE) - expect_snapshot(rate_sleep(rate), error = TRUE) + expect_snapshot(rate_sleep(rd), error = TRUE) + expect_snapshot(rate_sleep(rd), error = TRUE) }) test_that("rate_backoff() backs off", { - rate <- rate_backoff( + rb <- rate_backoff( pause_base = 0.02, pause_min = 0, jitter = FALSE ) - msg <- catch_cnd(rate_sleep(rate)) + msg <- catch_cnd(rate_sleep(rb)) expect_true(inherits_all(msg, c("purrr_condition_rate_init", "condition"))) - msg <- catch_cnd(rate_sleep(rate, quiet = FALSE)) + msg <- catch_cnd(rate_sleep(rb, quiet = FALSE)) expect_true(inherits_all(msg, c("purrr_message_rate_retry", "message"))) expect_identical(msg$length, 0.04) - msg <- catch_cnd(rate_sleep(rate, quiet = FALSE)) + msg <- catch_cnd(rate_sleep(rb, quiet = FALSE)) expect_identical(msg$length, 0.08) - expect_snapshot(rate_sleep(rate), error = TRUE) - expect_snapshot(rate_sleep(rate), error = TRUE) + expect_snapshot(rate_sleep(rb), error = TRUE) + expect_snapshot(rate_sleep(rb), error = TRUE) }) test_that("rate_sleep() checks that rate is still valid", { - rate <- rate_delay(1, max_times = 0) - expect_snapshot(rate_sleep(rate), error = TRUE) - expect_snapshot(rate_sleep(rate), error = TRUE) + rd <- rate_delay(1, max_times = 0) + expect_snapshot(rate_sleep(rd), error = TRUE) + expect_snapshot(rate_sleep(rd), error = TRUE) })