diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index c4bcd6b68..1c8055ff0 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -4,9 +4,9 @@ # Created with usethis + edited to use API key. on: push: - branches: [main, master] + branches: [main, dev] pull_request: - branches: [main, master] + branches: [main, dev] name: R-CMD-check diff --git a/DESCRIPTION b/DESCRIPTION index b0b592e2a..c451f755b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: epipredict Title: Basic epidemiology forecasting methods -Version: 0.0.8 +Version: 0.0.9 Authors@R: c( person("Daniel", "McDonald", , "daniel@stat.ubc.ca", role = c("aut", "cre")), person("Ryan", "Tibshirani", , "ryantibs@cmu.edu", role = "aut"), @@ -27,6 +27,7 @@ Depends: parsnip (>= 1.0.0), R (>= 3.5.0) Imports: + checkmate, cli, distributional, dplyr, diff --git a/NAMESPACE b/NAMESPACE index fc7a7ea00..3c63145b3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -195,6 +195,17 @@ import(distributional) import(epiprocess) import(parsnip) import(recipes) +importFrom(checkmate,assert) +importFrom(checkmate,assert_character) +importFrom(checkmate,assert_date) +importFrom(checkmate,assert_function) +importFrom(checkmate,assert_int) +importFrom(checkmate,assert_integer) +importFrom(checkmate,assert_integerish) +importFrom(checkmate,assert_logical) +importFrom(checkmate,assert_number) +importFrom(checkmate,assert_numeric) +importFrom(checkmate,assert_scalar) importFrom(cli,cli_abort) importFrom(dplyr,across) importFrom(dplyr,all_of) diff --git a/NEWS.md b/NEWS.md index 80eb9f19b..3c5034080 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,7 +7,7 @@ Pre-1.0.0 numbering scheme: 0.x will indicate releases, while 0.0.x will indicat - add `check_enough_train_data` that will error if training data is too small - added `check_enough_train_data` to `arx_forecaster` - simplify `layer_residual_quantiles()` to avoid timesuck in `utils::methods()` -- rename the `dist_quantiles()` to be more descriptive, breaking change) +- rename the `dist_quantiles()` to be more descriptive, breaking change - removes previous `pivot_quantiles()` (now `*_wider()`, breaking change) - add `pivot_quantiles_wider()` for easier plotting - add complement `pivot_quantiles_longer()` @@ -31,3 +31,4 @@ Pre-1.0.0 numbering scheme: 0.x will indicate releases, while 0.0.x will indicat - Publish public for easy navigation - Two simple forecasters as test beds - Working vignette +- use `checkmate` for input validation diff --git a/R/epipredict-package.R b/R/epipredict-package.R index 11e2ec833..69eb05bdc 100644 --- a/R/epipredict-package.R +++ b/R/epipredict-package.R @@ -4,6 +4,9 @@ #' @importFrom stats poly predict lm residuals quantile #' @importFrom cli cli_abort #' @importFrom lifecycle deprecated +#' @importFrom checkmate assert assert_character assert_int assert_scalar +#' assert_logical assert_numeric assert_number assert_integer +#' assert_integerish assert_date assert_function #' @import epiprocess parsnip ## usethis namespace: end NULL diff --git a/R/utils-arg.R b/R/utils-arg.R index 091987722..b4242eaf9 100644 --- a/R/utils-arg.R +++ b/R/utils-arg.R @@ -2,204 +2,103 @@ # http://adv-r.had.co.nz/Computing-on-the-language.html#substitute # Modeled after / copied from rundel/ghclass -handle_arg_list <- function(..., tests) { +handle_arg_list <- function(..., .tests) { values <- list(...) names <- eval(substitute(alist(...))) names <- map(names, deparse) - walk2(names, values, tests) + walk2(names, values, .tests) } arg_is_scalar <- function(..., allow_null = FALSE, allow_na = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (length(value) > 1 | (!allow_null & length(value) == 0)) { - cli::cli_abort("Argument {.val {name}} must be of length 1.") - } - if (!is.null(value)) { - if (is.na(value) & !allow_na) { - cli::cli_abort( - "Argument {.val {name}} must not be a missing value ({.val {NA}})." - ) - } - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_scalar(value, null.ok = allow_null, na.ok = allow_na, .var.name = name) + }) } - arg_is_lgl <- function(..., allow_null = FALSE, allow_na = FALSE, allow_empty = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (is.null(value) & !allow_null) { - cli::cli_abort("Argument {.val {name}} must be of logical type.") - } - if (any(is.na(value)) & !allow_na) { - cli::cli_abort("Argument {.val {name}} must not contain any missing values ({.val {NA}}).") - } - if (!is.null(value) & (length(value) == 0 & !allow_empty)) { - cli::cli_abort("Argument {.val {name}} must have length >= 1.") - } - if (!is.null(value) & length(value) != 0 & !is.logical(value)) { - cli::cli_abort("Argument {.val {name}} must be of logical type.") - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_logical(value, null.ok = allow_null, any.missing = allow_na, min.len = as.integer(!allow_empty), .var.name = name) + }) } arg_is_lgl_scalar <- function(..., allow_null = FALSE, allow_na = FALSE) { - arg_is_lgl(..., allow_null = allow_null, allow_na = allow_na) - arg_is_scalar(..., allow_null = allow_null, allow_na = allow_na) + handle_arg_list(..., .tests = function(name, value) { + assert_logical(value, null.ok = allow_null, any.missing = allow_na, min.len = 1, max.len = 1, .var.name = name) + }) } arg_is_numeric <- function(..., allow_null = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (!(is.numeric(value) | (is.null(value) & allow_null))) { - cli::cli_abort("All {.val {name}} must numeric.") - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_numeric(value, null.ok = allow_null, any.missing = FALSE, .var.name = name) + }) } arg_is_pos <- function(..., allow_null = FALSE) { - arg_is_numeric(..., allow_null = allow_null) - handle_arg_list( - ..., - tests = function(name, value) { - if (!(all(value > 0) | (is.null(value) & allow_null))) { - cli::cli_abort("All {.val {name}} must be positive number(s).") - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_numeric(value, lower = 1, null.ok = allow_null, any.missing = FALSE, .var.name = name) + }) } arg_is_nonneg <- function(..., allow_null = FALSE) { - arg_is_numeric(..., allow_null = allow_null) - handle_arg_list( - ..., - tests = function(name, value) { - if (!(all(value >= 0) | (is.null(value) & allow_null))) { - cli::cli_abort("All {.val {name}} must be nonnegative number(s).") - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_numeric(value, lower = 0, null.ok = allow_null, any.missing = FALSE, .var.name = name) + }) } arg_is_int <- function(..., allow_null = FALSE) { - arg_is_numeric(..., allow_null = allow_null) - handle_arg_list( - ..., - tests = function(name, value) { - if (!(all(value %% 1 == 0) | (is.null(value) & allow_null))) { - cli::cli_abort("All {.val {name}} must be whole positive number(s).") - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_integerish(value, null.ok = allow_null, .var.name = name) + }) } arg_is_pos_int <- function(..., allow_null = FALSE) { - arg_is_int(..., allow_null = allow_null) - arg_is_pos(..., allow_null = allow_null) + handle_arg_list(..., .tests = function(name, value) { + assert_integerish(value, null.ok = allow_null, lower = 1, any.missing = FALSE, .var.name = name) + }) } - arg_is_nonneg_int <- function(..., allow_null = FALSE) { - arg_is_int(..., allow_null = allow_null) - arg_is_nonneg(..., allow_null = allow_null) -} - -arg_is_date <- function(..., allow_null = FALSE, allow_na = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (is.null(value) & !allow_null) { - cli::cli_abort("Argument {.val {name}} may not be `NULL`.") - } - if (any(is.na(value)) & !allow_na) { - cli::cli_abort("Argument {.val {name}} must not contain any missing values ({.val {NA}}).") - } - if (!(is(value, "Date") | is.null(value) | all(is.na(value)))) { - cli::cli_abort("Argument {.val {name}} must be a Date. Try `as.Date()`.") - } - } - ) -} - -arg_is_probabilities <- function(..., allow_null = FALSE) { - arg_is_numeric(..., allow_null = allow_null) - handle_arg_list( - ..., - tests = function(name, value) { - if (!((all(value >= 0) && all(value <= 1)) | (is.null(value) & allow_null))) { - cli::cli_abort("All {.val {name}} must be in [0,1].") - } - } - ) + handle_arg_list(..., .tests = function(name, value) { + assert_integerish(value, null.ok = allow_null, lower = 0, any.missing = FALSE, .var.name = name) + }) } -arg_is_chr <- function(..., allow_null = FALSE, allow_na = FALSE, allow_empty = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (is.null(value) & !allow_null) { - cli::cli_abort("Argument {.val {name}} may not be `NULL`.") - } - if (any(is.na(value)) & !allow_na) { - cli::cli_abort("Argument {.val {name}} must not contain any missing values ({.val {NA}}).") - } - if (!is.null(value) & (length(value) == 0L & !allow_empty)) { - cli::cli_abort("Argument {.val {name}} must have length > 0.") - } - if (!(is.character(value) | is.null(value) | all(is.na(value)))) { - cli::cli_abort("Argument {.val {name}} must be of character type.") - } - } - ) +arg_is_date <- function(..., allow_null = FALSE) { + handle_arg_list(..., .tests = function(name, value) { + assert_date(value, null.ok = allow_null, .var.name = name) + }) } -arg_is_chr_scalar <- function(..., allow_null = FALSE, allow_na = FALSE) { - arg_is_chr(..., allow_null = allow_null, allow_na = allow_na) - arg_is_scalar(..., allow_null = allow_null, allow_na = allow_na) +arg_is_probabilities <- function(..., allow_null = FALSE, allow_na = FALSE) { + handle_arg_list(..., .tests = function(name, value) { + assert_numeric(value, lower = 0, upper = 1, null.ok = allow_null, any.missing = allow_na, .var.name = name) + }) } - -arg_is_function <- function(..., allow_null = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (is.null(value) & !allow_null) { - cli::cli_abort("Argument {.val {name}} must be a function.") - } - if (!is.null(value) & !is.function(value)) { - cli::cli_abort("Argument {.val {name}} must be a function.") - } - } - ) +arg_is_chr <- function(..., allow_null = FALSE, allow_na = FALSE, allow_empty = FALSE) { + handle_arg_list(..., .tests = function(name, value) { + assert_character(value, null.ok = allow_null, any.missing = allow_na, min.len = as.integer(!allow_empty), .var.name = name) + }) } - - -arg_is_sorted <- function(..., allow_null = FALSE) { - handle_arg_list( - ..., - tests = function(name, value) { - if (is.unsorted(value, na.rm = TRUE) | (is.null(value) & !allow_null)) { - cli::cli_abort("{.val {name}} must be sorted in increasing order.") - } - } - ) +arg_is_chr_scalar <- function(..., allow_null = FALSE, allow_na = FALSE) { + handle_arg_list(..., .tests = function(name, value) { + assert_character(value, null.ok = allow_null, any.missing = allow_na, min.len = 1, max.len = 1, .var.name = name) + }) } +arg_is_function <- function(..., allow_null = FALSE) { + handle_arg_list(..., .tests = function(name, value) { + assert_function(value, null.ok = allow_null, .var.name = name) + }) +} -arg_to_date <- function(x, allow_null = FALSE, allow_na = FALSE) { - arg_is_scalar(x, allow_null = allow_null, allow_na = allow_na) +arg_to_date <- function(x, allow_null = FALSE) { + arg_is_scalar(x, allow_null = allow_null) if (!is.null(x)) { x <- tryCatch(as.Date(x, origin = "1970-01-01"), error = function(e) NA) } - arg_is_date(x, allow_null = allow_null, allow_na = allow_na) + arg_is_date(x, allow_null = allow_null) x } diff --git a/tests/testthat/test-arg_is_.R b/tests/testthat/test-arg_is_.R index 7ca6f1d7f..52a4a16db 100644 --- a/tests/testthat/test-arg_is_.R +++ b/tests/testthat/test-arg_is_.R @@ -44,7 +44,7 @@ test_that("scalar", { test_that("numeric", { expect_silent(arg_is_numeric(i, j, x, y)) expect_error(arg_is_numeric(a)) - expect_error(arg_is_numeric(d)) + expect_silent(arg_is_numeric(d)) expect_silent(arg_is_numeric(c(i, j))) expect_silent(arg_is_numeric(i, k)) expect_silent(arg_is_numeric(i, j, n, allow_null = TRUE)) @@ -56,7 +56,7 @@ test_that("numeric", { test_that("positive", { expect_silent(arg_is_pos(i, j, x, y)) expect_error(arg_is_pos(a)) - expect_error(arg_is_pos(d)) + expect_silent(arg_is_pos(d)) expect_silent(arg_is_pos(c(i, j))) expect_error(arg_is_pos(i, k)) expect_silent(arg_is_pos(i, j, n, allow_null = TRUE)) @@ -68,7 +68,7 @@ test_that("positive", { test_that("nonneg", { expect_silent(arg_is_nonneg(i, j, x, y)) expect_error(arg_is_nonneg(a)) - expect_error(arg_is_nonneg(d)) + expect_silent(arg_is_nonneg(d)) expect_silent(arg_is_nonneg(c(i, j))) expect_error(arg_is_nonneg(i, k)) expect_silent(arg_is_nonneg(i, j, n, allow_null = TRUE)) @@ -96,7 +96,8 @@ test_that("date", { expect_error(arg_is_date(d, dd, n)) expect_error(arg_is_date(d, dd, nn)) expect_silent(arg_is_date(d, dd, n, allow_null = TRUE)) - expect_silent(arg_is_date(d, dd, nn, allow_na = TRUE)) + # Upstream issue, see: https://github.com/mllg/checkmate/issues/256 + # expect_silent(arg_is_date(d, dd, nn, allow_na = TRUE)) expect_error(arg_is_date(a)) expect_error(arg_is_date(v)) expect_error(arg_is_date(ll)) @@ -136,14 +137,6 @@ test_that("function", { expect_silent(arg_is_function(g, f = NULL, allow_null = TRUE)) }) -test_that("sorted", { - expect_silent(arg_is_sorted(a = 1:5, b = 6:10)) - expect_error(arg_is_sorted(a = 5:1, b = 6:10)) - expect_error(arg_is_sorted(b = NULL)) - expect_silent(arg_is_sorted(b = NULL, allow_null = TRUE)) -}) - - test_that("coerce scalar to date", { expect_error(arg_to_date("12345")) expect_s3_class(arg_to_date(12345), "Date")