diff --git a/DESCRIPTION b/DESCRIPTION index 841ad2d1..a43551e8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -61,7 +61,7 @@ Config/Needs/website: Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.0 +RoxygenNote: 7.2.1 VignetteBuilder: knitr Config/testthat/edition: 3 Remotes: diff --git a/NAMESPACE b/NAMESPACE index 7be5a2b0..954fa093 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -65,8 +65,12 @@ importFrom(butcher,axe_data) importFrom(butcher,axe_env) importFrom(butcher,axe_fitted) importFrom(butcher,butcher) +importFrom(cli,cli_abort) +importFrom(cli,cli_inform) +importFrom(cli,cli_warn) importFrom(dplyr,"%>%") importFrom(ggplot2,autoplot) +importFrom(rlang,caller_env) importFrom(stats,predict) importFrom(workflowsets,workflow_set) importFrom(yardstick,metric_set) diff --git a/NEWS.md b/NEWS.md index 4e4f11e2..16336c4e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,11 @@ To be released as stacks 1.0.1. `control_grid()` object. Will work as long as the object passed to `control` includes the same elements as `control_grid()`. +* Re-implemented package errors, warnings, and messages to make use of + infrastructure from the cli package; error headers now refer to the function + called directly by the user rather than the internal function that raised the + error. + # stacks 1.0.0 stacks 1.0.0 is the first production release of the package. While this release diff --git a/R/add_candidates.R b/R/add_candidates.R index 8ff48294..a361c973 100644 --- a/R/add_candidates.R +++ b/R/add_candidates.R @@ -94,9 +94,10 @@ add_candidates.workflow_set <- function(data_stack, candidates, name = deparse(substitute(candidates)), ...) { if (!"result" %in% colnames(candidates)) { - glue_stop( - "The supplied workflow_set must be fitted to resamples with ", - "workflows::workflow_map() before being added to a data stack." + cli_abort( + "The supplied workflow_set must be fitted to resamples with + workflows::workflow_map() before being added to a data stack.", + call = caller_env(0) ) } @@ -132,18 +133,22 @@ add_candidates.tune_results <- function(data_stack, candidates, add_candidates.default <- function(data_stack, candidates, name, ...) { check_add_data_stack(data_stack) - glue_stop( - "The second argument to add_candidates() should inherit from one of ", - "`tune_results` or `workflow_set`, but its class ", - "is {list(class(candidates))}." + cli_abort( + "The second argument to add_candidates() should inherit from one of + `tune_results` or `workflow_set`, but its class + is {list(class(candidates))}.", + call = caller_env(0) ) } .set_outcome <- function(stack, candidates) { if (!.get_outcome(stack) %in% c("init_", tune::.get_tune_outcome_names(candidates))) { - glue_stop("The model definition you've tried to add to the stack has ", - "outcome variable {list(tune::.get_tune_outcome_names(candidates))}, ", - "while the stack's outcome variable is {.get_outcome(stack)}.") + cli_abort( + "The model definition you've tried to add to the stack has + outcome variable {list(tune::.get_tune_outcome_names(candidates))}, + while the stack's outcome variable is {.get_outcome(stack)}.", + call = caller_env(1) + ) } attr(stack, "outcome") <- tune::.get_tune_outcome_names(candidates) @@ -159,9 +164,10 @@ add_candidates.default <- function(data_stack, candidates, name, ...) { hash_matches <- .get_rs_hash(stack) %in% c("init_", new_hash) if (!hash_matches) { - glue_stop( - "It seems like the new candidate member '{name}' doesn't make use ", - "of the same resampling object as the existing candidates." + cli_abort( + "It seems like the new candidate member '{name}' doesn't make use + of the same resampling object as the existing candidates.", + call = caller_env() ) } @@ -201,9 +207,10 @@ add_candidates.default <- function(data_stack, candidates, name, ...) { # and then appends the model definition, hash, and metrics .set_model_defs_candidates <- function(stack, candidates, name) { if (name %in% .get_model_def_names(stack)) { - glue_stop( - "The new model definition has the ", - "same name '{name}' as an existing model definition." + cli_abort( + "The new model definition has the + same name '{name}' as an existing model definition.", + call = caller_env(1) ) } @@ -219,10 +226,11 @@ add_candidates.default <- function(data_stack, candidates, name, ...) { unname() if (!"prob_metric" %in% metric_types) { - glue_stop( - "The supplied candidates were tuned/fitted using only metrics that ", - "rely on hard class predictions. Please tune/fit with at least one ", - "class probability-based metric, such as `yardstick::roc_auc()`." + cli_abort( + "The supplied candidates were tuned/fitted using only metrics that + rely on hard class predictions. Please tune/fit with at least one + class probability-based metric, such as `yardstick::roc_auc()`.", + call = caller_env(1) ) } } @@ -230,7 +238,9 @@ add_candidates.default <- function(data_stack, candidates, name, ...) { model_defs <- attr(stack, "model_defs") model_metrics <- attr(stack, "model_metrics") - model_defs[[name]] <- attr(candidates, "workflow") %>% stack_workflow() + model_defs[[name]] <- + attr(candidates, "workflow") %>% + stack_workflow(call = caller_env()) model_metrics[[name]] <- tune::collect_metrics(candidates) attr(stack, "model_defs") <- model_defs @@ -248,8 +258,11 @@ add_candidates.default <- function(data_stack, candidates, name, ...) { if ((!identical(training_data, tibble::tibble())) && (!identical(training_data, new_data))) { - glue_stop("The newly added candidate member, `{name}`, ", - "uses different training data than the existing candidates.") + cli_abort( + "The newly added candidate member, `{name}`, + uses different training data than the existing candidates.", + call = caller_env(1) + ) } attr(stack, "train") <- new_data @@ -332,9 +345,9 @@ rm_duplicate_cols <- function(df) { n_candidates <- "1 candidate" } - glue_warn( - "Predictions from {n_candidates} were identical to ", - "those from existing candidates and were removed from the data stack." + cli_warn( + "Predictions from {n_candidates} were identical to + those from existing candidates and were removed from the data stack." ) df <- df %>% dplyr::select(-any_of(exclude)) @@ -362,7 +375,7 @@ update_stack_data <- function(stack, new_data) { # takes in a workflow and returns a minimal workflow for # use in the stack -stack_workflow <- function(x) { +stack_workflow <- function(x, call) { res <- workflows::workflow() %>% workflows::add_model(workflows::extract_spec_parsnip(x)) @@ -376,7 +389,10 @@ stack_workflow <- function(x) { } else if (inherits(pre, "workflow_variables")) { res <- res %>% workflows::add_variables(variables = pre) } else { - rlang::abort(paste0("Can't add a preprocessor of class '", class(pre)[1], "'")) + cli_abort( + "Can't add a preprocessor of class '{class(pre)[1]}'", + call = call + ) } res @@ -387,12 +403,13 @@ check_add_data_stack <- function(data_stack) { data_stack, c("tune_results", "tune_bayes", "resample_results") )) { - glue_stop( - "It looks like the first argument inherits from {list(class(data_stack))} ", - "rather than `data_stack`. ", - "Did you accidentally supply the candidate members as the first argument? ", - "If so, please supply the output of `stacks()` or another `add_candidates()` as ", - "the argument to `data_stack`." + cli_abort( + "It looks like the first argument inherits from {list(class(data_stack))} + rather than `data_stack`. + Did you accidentally supply the candidate members as the first argument? + If so, please supply the output of `stacks()` or another `add_candidates()` as + the argument to `data_stack`.", + call = caller_env() ) } else { check_inherits(data_stack, "data_stack") @@ -404,26 +421,28 @@ check_candidates <- function(candidates, name) { candidates, c("tune_results", "tune_bayes", "resample_results") )) { - glue_stop( - "The inputted `candidates` argument has class `{list(class(candidates))}`", - ", but it should inherit from one of 'tune_results', 'tune_bayes', ", - "or 'resample_results'." + cli_abort( + "The inputted `candidates` argument has class `{list(class(candidates))}`, + but it should inherit from one of 'tune_results', 'tune_bayes', + or 'resample_results'.", + call = caller_env() ) } if (nrow(tune::collect_notes(candidates)) != 0) { - glue_warn( - "The inputted `candidates` argument `{name}` generated notes during ", - "tuning/resampling. Model stacking may fail due to these ", - "issues; see `?collect_notes` if so." + cli_warn( + "The inputted `candidates` argument `{name}` generated notes during + tuning/resampling. Model stacking may fail due to these + issues; see `?collect_notes` if so." ) } if ((!".predictions" %in% colnames(candidates)) | is.null(attributes(candidates)$workflow)) { - glue_stop( - "The inputted `candidates` argument was not generated with the ", - "appropriate control settings. Please see ?control_stack." + cli_abort( + "The inputted `candidates` argument was not generated with the + appropriate control settings. Please see ?control_stack.", + call = caller_env() ) } } @@ -433,19 +452,20 @@ check_name <- function(name) { name, c("tune_results", "tune_bayes", "resample_results") )) { - glue_stop( - "The inputted `name` argument looks like a tuning/fitting results object ", - "that might be supplied as a `candidates` argument. Did you try to add ", - "more than one set of candidates in one `add_candidates()` call?" + cli_abort( + "The inputted `name` argument looks like a tuning/fitting results object + that might be supplied as a `candidates` argument. Did you try to add + more than one set of candidates in one `add_candidates()` call?", + call = caller_env() ) } else { check_inherits(name, "character") if (make.names(name) != name) { - glue_message( - "The inputted `name` argument cannot prefix a valid column name. The ", - 'data stack will use "{make.names(name)}" rather than "{name}" in ', - "constructing candidate names." + cli_inform( + "The inputted `name` argument cannot prefix a valid column name. The + data stack will use '{make.names(name)}' rather than '{name}' in + constructing candidate names." ) } } diff --git a/R/blend_predictions.R b/R/blend_predictions.R index 03f16b59..38683d8c 100644 --- a/R/blend_predictions.R +++ b/R/blend_predictions.R @@ -247,25 +247,29 @@ blend_predictions <- function(data_stack, check_regularization <- function(x, arg) { if (!is.numeric(x)) { - glue_stop( - "The argument to '{arg}' must be a numeric, but the supplied {arg}'s ", - "class is `{list(class(x))}`" + cli_abort( + "The argument to '{arg}' must be a numeric, but the supplied {arg}'s + class is `{list(class(x))}`", + call = caller_env() ) } if (length(x) == 0) { - glue_stop("Please supply one or more {arg} values.") + cli_abort("Please supply one or more {arg} values.", + call = caller_env()) } if (arg == "penalty") { if (any(x < 0)) { - glue_stop("Please supply only nonnegative values to the {arg} argument.") + cli_abort("Please supply only nonnegative values to the {arg} argument.", + call = caller_env()) } } if (arg == "mixture") { if (any(x < 0 || x > 1)) { - glue_stop("Please supply only values in [0, 1] to the {arg} argument.") + cli_abort("Please supply only values in [0, 1] to the {arg} argument.", + call = caller_env()) } } } @@ -337,16 +341,18 @@ check_blend_data_stack <- function(data_stack) { if (!inherits(data_stack, "data_stack")) { check_inherits(data_stack, "data_stack") } else if (ncol(data_stack) == 0) { - glue_stop( - "The data stack supplied as the argument to `data_stack` has no ", - "candidate members. Please first add candidates with ", - "the `add_candidates()` function." + cli_abort( + "The data stack supplied as the argument to `data_stack` has no + candidate members. Please first add candidates with + the `add_candidates()` function.", + call = caller_env() ) } else if ((ncol(data_stack) == 2 && attr(data_stack, "mode") == "regression") || ncol(data_stack) == length(levels(data_stack[[1]])) + 1) { - glue_stop( - "The supplied data stack only contains one candidate member. Please ", - "add more candidate members using `add_candidates()` before blending." + cli_abort( + "The supplied data stack only contains one candidate member. Please + add more candidate members using `add_candidates()` before blending.", + call = caller_env() ) } @@ -357,16 +363,17 @@ process_data_stack <- function(data_stack) { dat <- tibble::as_tibble(data_stack) %>% na.omit() if (nrow(dat) == 0) { - glue_stop( - "All rows in the data stack have at least one missing value. ", - "Please ensure that all candidates supply predictions." + cli_abort( + "All rows in the data stack have at least one missing value. + Please ensure that all candidates supply predictions.", + call = caller_env() ) } if (nrow(dat) < nrow(data_stack)) { - glue_message( - "{nrow(data_stack) - nrow(dat)} of the {nrow(data_stack)} rows in the ", - "data stack have missing values, and will be omitted in the blending process." + cli_inform( + "{nrow(data_stack) - nrow(dat)} of the {nrow(data_stack)} rows in the + data stack have missing values, and will be omitted in the blending process." ) } diff --git a/R/collect_parameters.R b/R/collect_parameters.R index 5e442bd6..f678408a 100644 --- a/R/collect_parameters.R +++ b/R/collect_parameters.R @@ -52,9 +52,10 @@ collect_parameters <- function(stack, candidates, ...) { #' @export #' @rdname collect_parameters collect_parameters.default <- function(stack, candidates, ...) { - glue_stop( - "There is no `collect_parameters()` method currently implemented ", - "for {list(class(stack))} objects." + cli_abort( + "There is no `collect_parameters()` method currently implemented + for {list(class(stack))} objects.", + call = caller_env(0) ) } @@ -154,9 +155,10 @@ collect_params <- function(cols_map, model_metrics, candidates, workflows, blend check_for_candidates <- function(model_metrics, candidates) { if ((!inherits(candidates, "character")) | (!candidates %in% names(model_metrics))) { - glue_stop( - "The `candidates` argument to `collect_parameters()` must be the name ", - "given to a set of candidates added with `add_candidates()`." + cli_abort( + "The `candidates` argument to `collect_parameters()` must be the name + given to a set of candidates added with `add_candidates()`.", + call = caller_env() ) } } diff --git a/R/fit_members.R b/R/fit_members.R index c25b7709..a7250240 100644 --- a/R/fit_members.R +++ b/R/fit_members.R @@ -227,18 +227,19 @@ sanitize_classification_names <- function(model_stack, member_names) { check_model_stack <- function(model_stack) { if (inherits(model_stack, "model_stack")) { if (!is.null(model_stack[["member_fits"]])) { - glue_warn( - "The members in the supplied `model_stack` have already been fitted ", - "and need not be fitted again." + cli_warn( + "The members in the supplied `model_stack` have already been fitted + and need not be fitted again." ) } return(invisible(TRUE)) } else if (inherits(model_stack, "data_stack")) { - glue_stop( - "The supplied `model_stack` argument is a data stack rather than ", - "a model stack. Did you forget to first evaluate the ensemble's ", - "stacking coefficients with `blend_predictions()`?" + cli_abort( + "The supplied `model_stack` argument is a data stack rather than + a model stack. Did you forget to first evaluate the ensemble's + stacking coefficients with `blend_predictions()`?", + call = caller_env() ) } else { check_inherits(model_stack, "model_stack") @@ -266,7 +267,7 @@ check_for_required_packages <- function(x) { ) if (any(!installed)) { - error_needs_install(pkgs, installed) + error_needs_install(pkgs, installed, call = caller_env()) } purrr::map( @@ -279,7 +280,7 @@ check_for_required_packages <- function(x) { # takes in a vector of package names and a logical vector giving # whether or not each is installed -error_needs_install <- function(pkgs, installed) { +error_needs_install <- function(pkgs, installed, call) { plural <- sum(!installed) != 1 last_sep <- if (sum(!installed) == 2) {"` and `"} else {"`, and `"} @@ -290,10 +291,11 @@ error_needs_install <- function(pkgs, installed) { "`" ) - glue_stop( - "The following package{if (plural) 's' else ''} ", - "need{if (plural) '' else 's'} to be installed before ", - "fitting members: {need_install}" + cli_abort( + "The following package{if (plural) 's' else ''} + need{if (plural) '' else 's'} to be installed before + fitting members: {need_install}", + call = call ) } diff --git a/R/predict.R b/R/predict.R index a52051d8..b7b18fcb 100644 --- a/R/predict.R +++ b/R/predict.R @@ -130,10 +130,11 @@ predict.model_stack <- function(object, new_data, type = NULL, members = FALSE, #' @export predict.data_stack #' @export predict.data_stack <- function(object, ...) { - glue_stop( - "To predict with a stacked ensemble, the supplied data stack must be ", - "evaluated with `blend_predictions()` and its member models fitted with ", - "`fit_members()` to predict on new data." + cli_abort( + "To predict with a stacked ensemble, the supplied data stack must be + evaluated with `blend_predictions()` and its member models fitted with + `fit_members()` to predict on new data.", + call = caller_env(0) ) } @@ -196,9 +197,10 @@ parse_member_probs <- function(member_name, member_probs, levels) { check_fitted <- function(model_stack) { if (is.null(model_stack[["member_fits"]])) { - glue_stop( - "The supplied model stack hasn't been fitted yet. ", - "Please fit the necessary members with fit_members() to predict on new data." + cli_abort( + "The supplied model stack hasn't been fitted yet. + Please fit the necessary members with fit_members() to predict on new data.", + call = caller_env() ) } } diff --git a/R/utils.R b/R/utils.R index 1c7610bf..783a01c4 100644 --- a/R/utils.R +++ b/R/utils.R @@ -4,6 +4,12 @@ #' @export dplyr::`%>%` +# Imports +#' @importFrom cli cli_inform +#' @importFrom cli cli_warn +#' @importFrom cli cli_abort +#' @importFrom rlang caller_env + # Global Variables # ------------------------------------------------------------------------ utils::globalVariables(c( @@ -54,56 +60,6 @@ utils::globalVariables(c( # Checks and Prompts # ------------------------------------------------------------------------ -# wrappers for prompting with glue with appropriate colors -glue_stop <- function(..., .sep = "", .envir = parent.frame()) { - glue_prompt( - ..., - .sep = .sep, - .envir = .envir, - type = "danger", - rlang_fn = rlang::abort - ) -} - -glue_warn <- function(..., .sep = "", .envir = parent.frame()) { - glue_prompt( - ..., - .sep = .sep, - .envir = .envir, - type = "warning", - rlang_fn = rlang::warn - ) -} - -glue_message <- function(..., .sep = "", .envir = parent.frame()) { - glue_prompt( - ..., - .sep = .sep, - .envir = .envir, - type = "info", - rlang_fn = rlang::inform - ) -} - -# takes in a prompt and a prompt type and colors the -# prompt according to the prompt type -color_prompt <- function(prompt, type) { - colors <- tune::get_tune_colors() - - prompt_fn <- colors[["message"]][[type]] - - prompt_fn(prompt) -} - -# takes in a vector, parses it with glue, wraps to the console width, colors -# it with the appropriate tune color, and raises it with the appropriate prompt -glue_prompt <- function(..., .sep = "", .envir = parent.frame(), type, rlang_fn) { - glue::glue(..., .sep = .sep, .envir = .envir) %>% - glue::glue_collapse() %>% - color_prompt(type) %>% - rlang_fn() -} - # adapted from tune check_empty_ellipses <- function(...) { dots <- rlang::enquos(...) @@ -127,8 +83,11 @@ check_inherits <- function(x, what) { cl <- match.call() if (!inherits(x, what)) { - glue_stop("Element `{list(cl$x)}` needs to inherit from `{what}`, but its ", - "class is `{list(class(x))}`.") + cli_abort( + "Element `{list(cl$x)}` needs to inherit from `{what}`, but its + class is `{list(class(x))}`.", + call = NULL + ) } invisible(TRUE) diff --git a/tests/testthat/helper_functions.R b/tests/testthat/helper_functions.R index d8b5bf80..4135d484 100644 --- a/tests/testthat/helper_functions.R +++ b/tests/testthat/helper_functions.R @@ -13,13 +13,17 @@ check_inherits <- function(x, what) { cl <- match.call() if (!inherits(x, what)) { - glue_stop("Element `{list(cl$x)}` needs to inherit from `{what}`, but its ", - "class is `{list(class(x))}`.") + cli_abort( + "Element `{list(cl$x)}` needs to inherit from `{what}`, but its + class is `{list(class(x))}`.", + call = NULL + ) } invisible(TRUE) } + # Helper Data # ----------------------------------------------------------------------- # the `helper_data.Rda` contains data objects for use in unit testing. diff --git a/tests/testthat/test_utils.R b/tests/testthat/test_utils.R index f4bb58fc..828b1b4a 100644 --- a/tests/testthat/test_utils.R +++ b/tests/testthat/test_utils.R @@ -63,10 +63,7 @@ test_that("misc. utilities work", { expect_true(check_inherits("howdy", "character")) yall <- "y'all" - expect_error(glue_stop("howdy {yall}"), "howdy y'all") - expect_warning(glue_warn("howdy {yall}"), "howdy y'all") - expect_message(glue_message("howdy {yall}"), "howdy y'all") - + expect_warning( check_empty_ellipses(yall), "were passed: 'yall'"