Skip to content

Commit 43ce902

Browse files
committed
Merge branch 'dev' into ndefries/epix-slide-pass-reftimevalue-without-env
2 parents 3a299c5 + a1a53c5 commit 43ce902

File tree

10 files changed

+231
-62
lines changed

10 files changed

+231
-62
lines changed

DESCRIPTION

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Description: This package introduces a common data structure for epidemiological
2121
work with revisions to these data sets over time, and offers associated
2222
utilities to perform basic signal processing tasks.
2323
License: MIT + file LICENSE
24-
Imports:
24+
Imports:
25+
cli,
2526
data.table,
2627
dplyr (>= 1.0.0),
2728
fabletools,
@@ -48,7 +49,7 @@ Suggests:
4849
knitr,
4950
outbreaks,
5051
rmarkdown,
51-
testthat (>= 3.0.0),
52+
testthat (>= 3.1.5),
5253
waldo (>= 0.3.1),
5354
withr
5455
VignetteBuilder:

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ importFrom(dplyr,ungroup)
8585
importFrom(lubridate,days)
8686
importFrom(lubridate,weeks)
8787
importFrom(magrittr,"%>%")
88+
importFrom(purrr,map_lgl)
8889
importFrom(rlang,"!!!")
8990
importFrom(rlang,"!!")
9091
importFrom(rlang,.data)
@@ -102,6 +103,7 @@ importFrom(rlang,f_rhs)
102103
importFrom(rlang,is_environment)
103104
importFrom(rlang,is_formula)
104105
importFrom(rlang,is_function)
106+
importFrom(rlang,is_missing)
105107
importFrom(rlang,is_quosure)
106108
importFrom(rlang,is_string)
107109
importFrom(rlang,missing_arg)
@@ -118,3 +120,4 @@ importFrom(tidyr,unnest)
118120
importFrom(tidyselect,eval_select)
119121
importFrom(tidyselect,starts_with)
120122
importFrom(tsibble,as_tsibble)
123+
importFrom(utils,tail)

R/grouped_epi_archive.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ grouped_epi_archive =
222222

223223
# Check that `f` takes enough args
224224
if (!missing(f) && is.function(f)) {
225-
check_sufficient_f_args(f, 3L)
225+
assert_sufficient_f_args(f, ..., n_mandatory_f_args = 3L)
226226
}
227227

228228
# Validate and pre-process `before`:

R/methods-epi_archive.R

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,8 @@ group_by.epi_archive = function(.data, ..., .add=FALSE, .drop=dplyr::group_by_dr
793793
#' as.Date("2020-06-15"),
794794
#' by = "1 day")
795795
#'
796+
#' # A simple (but not very useful) example (see the archive vignette for a more
797+
#' # realistic one):
796798
#' archive_cases_dv_subset %>%
797799
#' group_by(geo_value) %>%
798800
#' epix_slide(f = ~ mean(.x$case_rate_7d_av),
@@ -804,39 +806,71 @@ group_by.epi_archive = function(.data, ..., .add=FALSE, .drop=dplyr::group_by_dr
804806
#' # values. The actual number of `time_value`s in each computation depends on
805807
#' # the reporting latency of the signal and `time_value` range covered by the
806808
#' # archive (2020-06-01 -- 2021-11-30 in this example). In this case, we have
807-
#' # 0 `time_value`s, for ref time 2020-06-01 --> the result is automatically discarded
808-
#' # 1 `time_value`, for ref time 2020-06-02
809-
#' # 2 `time_value`s, for the rest of the results
810-
#' # never 3 `time_value`s, due to data latency
811-
#'
812-
#'
809+
#' # * 0 `time_value`s, for ref time 2020-06-01 --> the result is automatically
810+
#' # discarded
811+
#' # * 1 `time_value`, for ref time 2020-06-02
812+
#' # * 2 `time_value`s, for the rest of the results
813+
#' # * never the 3 `time_value`s we would get from `epi_slide`, since, because
814+
#' # of data latency, we'll never have an observation
815+
#' # `time_value == ref_time_value` as of `ref_time_value`.
816+
#' # The example below shows this type of behavior in more detail.
817+
#'
818+
#' # Examining characteristics of the data passed to each computation with
819+
#' # `all_versions=FALSE`.
820+
#' archive_cases_dv_subset %>%
821+
#' group_by(geo_value) %>%
822+
#' epix_slide(
823+
#' function(x, gk, rtv) {
824+
#' tibble(
825+
#' time_range = if(nrow(x) == 0L) {
826+
#' "0 `time_value`s"
827+
#' } else {
828+
#' sprintf("%s -- %s", min(x$time_value), max(x$time_value))
829+
#' },
830+
#' n = nrow(x),
831+
#' class1 = class(x)[[1L]]
832+
#' )
833+
#' },
834+
#' before = 5, all_versions = FALSE,
835+
#' ref_time_values = ref_time_values, names_sep=NULL) %>%
836+
#' ungroup() %>%
837+
#' arrange(geo_value, time_value)
813838
#'
814839
#' # --- Advanced: ---
815840
#'
816841
#' # `epix_slide` with `all_versions=FALSE` (the default) applies a
817842
#' # version-unaware computation to several versions of the data. We can also
818843
#' # use `all_versions=TRUE` to apply a version-*aware* computation to several
819-
#' # versions of the data. In this case, each computation should expect an
844+
#' # versions of the data, again looking at characteristics of the data passed
845+
#' # to each computation. In this case, each computation should expect an
820846
#' # `epi_archive` containing the relevant version data:
821847
#'
822848
#' archive_cases_dv_subset %>%
823849
#' group_by(geo_value) %>%
824850
#' epix_slide(
825851
#' function(x, gk, rtv) {
826852
#' tibble(
827-
#' versions_end = max(x$versions_end),
853+
#' versions_start = if (nrow(x$DT) == 0L) {
854+
#' "NA (0 rows)"
855+
#' } else {
856+
#' toString(min(x$DT$version))
857+
#' },
858+
#' versions_end = x$versions_end,
828859
#' time_range = if(nrow(x$DT) == 0L) {
829860
#' "0 `time_value`s"
830861
#' } else {
831862
#' sprintf("%s -- %s", min(x$DT$time_value), max(x$DT$time_value))
832863
#' },
864+
#' n = nrow(x$DT),
833865
#' class1 = class(x)[[1L]]
834866
#' )
835867
#' },
836-
#' before = 2, all_versions = TRUE,
868+
#' before = 5, all_versions = TRUE,
837869
#' ref_time_values = ref_time_values, names_sep=NULL) %>%
838870
#' ungroup() %>%
839-
#' arrange(geo_value, time_value)
871+
#' # Focus on one geo_value so we can better see the columns above:
872+
#' filter(geo_value == "ca") %>%
873+
#' select(-geo_value)
840874
#'
841875
#' @importFrom rlang enquo !!!
842876
#' @export

R/slide.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ epi_slide = function(x, f, ..., before, after, ref_time_values,
158158

159159
# Check that `f` takes enough args
160160
if (!missing(f) && is.function(f)) {
161-
check_sufficient_f_args(f)
161+
assert_sufficient_f_args(f, ...)
162162
}
163163

164164
# Arrange by increasing time_value

R/utils.R

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,35 +100,87 @@ paste_lines = function(lines) {
100100
Abort = function(msg, ...) rlang::abort(break_str(msg, init = "Error: "), ...)
101101
Warn = function(msg, ...) rlang::warn(break_str(msg, init = "Warning: "), ...)
102102

103-
#' Check that a sliding computation function takes enough args
103+
#' Assert that a sliding computation function takes enough args
104104
#'
105105
#' @param f Function; specifies a computation to slide over an `epi_df` or
106106
#' `epi_archive` in `epi_slide` or `epix_slide`.
107+
#' @param ... Dots that will be forwarded to `f` from the dots of `epi_slide` or
108+
#' `epix_slide`.
107109
#' @param n_mandatory_f_args Integer; specifies the number of arguments `f`
108110
#' is required to take before any `...` arg. Defaults to 2.
109111
#'
112+
#' @importFrom rlang is_missing
113+
#' @importFrom purrr map_lgl
114+
#' @importFrom utils tail
115+
#'
110116
#' @noRd
111-
check_sufficient_f_args <- function(f, n_mandatory_f_args = 2L) {
112-
arg_names = names(formals(args(f)))
113-
if ("..." %in% arg_names) {
117+
assert_sufficient_f_args <- function(f, ..., n_mandatory_f_args = 2L) {
118+
mandatory_f_args_labels <- c("window data", "group key", "reference time value")[seq(n_mandatory_f_args)]
119+
n_mandatory_f_args <- length(mandatory_f_args_labels)
120+
args = formals(args(f))
121+
args_names = names(args)
122+
# Remove named arguments forwarded from `epi[x]_slide`'s `...`:
123+
forwarded_dots_names = names(rlang::call_match(dots_expand = FALSE)[["..."]])
124+
args_matched_in_dots =
125+
# positional calling args will skip over args matched by named calling args
126+
args_names %in% forwarded_dots_names &
127+
# extreme edge case: `epi[x]_slide(<stuff>, dot = 1, `...` = 2)`
128+
args_names != "..."
129+
remaining_args = args[!args_matched_in_dots]
130+
remaining_args_names = names(remaining_args)
131+
# note that this doesn't include unnamed args forwarded through `...`.
132+
dots_i <- which(remaining_args_names == "...") # integer(0) if no match
133+
n_f_args_before_dots <- dots_i - 1L
134+
if (length(dots_i) != 0L) { # `f` has a dots "arg"
114135
# Keep all arg names before `...`
115-
dots_i <- which(arg_names == "...")
116-
arg_names <- arg_names[seq_len(dots_i - 1)]
117-
118-
if (length(arg_names) < n_mandatory_f_args) {
119-
Warn(sprintf("`f` only takes %s positional arguments before the `...` args, but %s were expected; this can lead to obtuse errors downstream", length(arg_names), n_mandatory_f_args),
120-
class="check_sufficient_f_args__f_needs_min_args_before_dots",
121-
epiprocess__f = f,
122-
epiprocess__arg_names = arg_names)
136+
mandatory_args_mapped_names <- remaining_args_names[seq_len(n_f_args_before_dots)]
137+
138+
if (n_f_args_before_dots < n_mandatory_f_args) {
139+
mandatory_f_args_in_f_dots =
140+
tail(mandatory_f_args_labels, n_mandatory_f_args - n_f_args_before_dots)
141+
cli::cli_warn(
142+
"`f` might not have enough positional arguments before its `...`; in the current `epi[x]_slide` call, the {mandatory_f_args_in_f_dots} will be included in `f`'s `...`; if `f` doesn't expect those arguments, it may produce confusing error messages",
143+
class = "epiprocess__assert_sufficient_f_args__mandatory_f_args_passed_to_f_dots",
144+
epiprocess__f = f,
145+
epiprocess__mandatory_f_args_in_f_dots = mandatory_f_args_in_f_dots
146+
)
123147
}
124-
} else {
125-
if (length(arg_names) < n_mandatory_f_args) {
126-
Abort(sprintf("`f` must take at least %s arguments", n_mandatory_f_args),
127-
class="check_sufficient_f_args__f_needs_min_args",
128-
epiprocess__f = f,
129-
epiprocess__arg_names = arg_names)
148+
} else { # `f` doesn't have a dots "arg"
149+
if (length(args_names) < n_mandatory_f_args + rlang::dots_n(...)) {
150+
# `f` doesn't take enough args.
151+
if (rlang::dots_n(...) == 0L) {
152+
# common case; try for friendlier error message
153+
Abort(sprintf("`f` must take at least %s arguments", n_mandatory_f_args),
154+
class = "epiprocess__assert_sufficient_f_args__f_needs_min_args",
155+
epiprocess__f = f)
156+
} else {
157+
# less common; highlight that they are (accidentally?) using dots forwarding
158+
Abort(sprintf("`f` must take at least %s arguments plus the %s arguments forwarded through `epi[x]_slide`'s `...`, or a named argument to `epi[x]_slide` was misspelled", n_mandatory_f_args, rlang::dots_n(...)),
159+
class = "epiprocess__assert_sufficient_f_args__f_needs_min_args_plus_forwarded",
160+
epiprocess__f = f)
161+
}
130162
}
131163
}
164+
# Check for args with defaults that are filled with mandatory positional
165+
# calling args. If `f` has fewer than n_mandatory_f_args before `...`, then we
166+
# only need to check those args for defaults. Note that `n_f_args_before_dots` is
167+
# length 0 if `f` doesn't accept `...`.
168+
n_remaining_args_for_default_check = min(c(n_f_args_before_dots, n_mandatory_f_args))
169+
default_check_args = remaining_args[seq_len(n_remaining_args_for_default_check)]
170+
default_check_args_names = names(default_check_args)
171+
has_default_replaced_by_mandatory = map_lgl(default_check_args, ~!is_missing(.x))
172+
if (any(has_default_replaced_by_mandatory)) {
173+
default_check_mandatory_args_labels =
174+
mandatory_f_args_labels[seq_len(n_remaining_args_for_default_check)]
175+
# ^ excludes any mandatory args absorbed by f's `...`'s:
176+
mandatory_args_replacing_defaults =
177+
default_check_mandatory_args_labels[has_default_replaced_by_mandatory]
178+
args_with_default_replaced_by_mandatory =
179+
rlang::syms(default_check_args_names[has_default_replaced_by_mandatory])
180+
cli::cli_abort("`epi[x]_slide` would pass the {mandatory_args_replacing_defaults} to `f`'s {args_with_default_replaced_by_mandatory} argument{?s}, which {?has a/have} default value{?s}; we suspect that `f` doesn't expect {?this arg/these args} at all and may produce confusing error messages. Please add additional arguments to `f` or remove defaults as appropriate.",
181+
class = "epiprocess__assert_sufficient_f_args__required_args_contain_defaults",
182+
epiprocess__f = f)
183+
}
132184
}
133185

134186
#' Convert to function

man/epix_slide.Rd

Lines changed: 43 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-epi_slide.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,5 @@ test_that("epi_slide alerts if the provided f doesn't take enough args", {
9595

9696
f_x_dots = function(x, ...) dplyr::tibble(value=mean(x$value), count=length(x$value))
9797
expect_warning(epi_slide(grouped, f_x_dots, before = 1L, ref_time_values = d+1),
98-
regexp = "positional arguments before the `...` args",
99-
class = "check_sufficient_f_args__f_needs_min_args_before_dots")
98+
class = "epiprocess__assert_sufficient_f_args__mandatory_f_args_passed_to_f_dots")
10099
})

tests/testthat/test-epix_slide.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,7 @@ test_that("epix_slide alerts if the provided f doesn't take enough args", {
357357

358358
f_x_dots = function(x, ...) dplyr::tibble(value=mean(x$binary), count=length(x$binary))
359359
expect_warning(epix_slide(xx, f_x_dots, before = 2L),
360-
regexp = "positional arguments before the `...` args",
361-
class = "check_sufficient_f_args__f_needs_min_args_before_dots")
360+
class = "epiprocess__assert_sufficient_f_args__mandatory_f_args_passed_to_f_dots")
362361
})
363362

364363
test_that("epix_slide computation can use ref_time_value", {

0 commit comments

Comments
 (0)