diff --git a/.lintr b/.lintr index 5fb827f3..df10b48b 100644 --- a/.lintr +++ b/.lintr @@ -1,8 +1,8 @@ -linters: with_defaults( +linters: linters_with_defaults( line_length_linter(120), T_and_F_symbol_linter, absolute_path_linter, nonportable_path_linter, - semicolon_terminator_linter, + semicolon_linter, undesirable_operator_linter ) diff --git a/NAMESPACE b/NAMESPACE index 664488f6..1408974d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,27 +2,29 @@ S3method(format,tblcheck_problem) S3method(print,tblcheck_problem) -S3method(tblcheck_grade,default) -S3method(tblcheck_grade,list) -S3method(tblcheck_grade,tblcheck_internal_problem) -S3method(tblcheck_grade,tblcheck_problem) -S3method(tblcheck_message,class_problem) -S3method(tblcheck_message,default) -S3method(tblcheck_message,dimensions_n_problem) -S3method(tblcheck_message,dimensions_problem) -S3method(tblcheck_message,groups_problem) -S3method(tblcheck_message,length_problem) -S3method(tblcheck_message,levels_n_problem) -S3method(tblcheck_message,levels_order_problem) -S3method(tblcheck_message,levels_problem) -S3method(tblcheck_message,levels_reversed_problem) -S3method(tblcheck_message,names_order_problem) -S3method(tblcheck_message,names_problem) -S3method(tblcheck_message,ncol_problem) -S3method(tblcheck_message,not_table_problem) -S3method(tblcheck_message,nrow_problem) -S3method(tblcheck_message,tblcheck_problem) -S3method(tblcheck_message,values_problem) +S3method(problem_grade,default) +S3method(problem_grade,gradethis_problem) +S3method(problem_grade,list) +S3method(problem_grade,tblcheck_internal_problem) +S3method(problem_grade,tblcheck_problem) +S3method(problem_message,class_problem) +S3method(problem_message,default) +S3method(problem_message,dimensions_n_problem) +S3method(problem_message,dimensions_problem) +S3method(problem_message,gradethis_problem) +S3method(problem_message,groups_problem) +S3method(problem_message,length_problem) +S3method(problem_message,levels_n_problem) +S3method(problem_message,levels_order_problem) +S3method(problem_message,levels_problem) +S3method(problem_message,levels_reversed_problem) +S3method(problem_message,names_order_problem) +S3method(problem_message,names_problem) +S3method(problem_message,ncol_problem) +S3method(problem_message,not_table_problem) +S3method(problem_message,nrow_problem) +S3method(problem_message,tblcheck_problem) +S3method(problem_message,values_problem) export("%>%") export(.result) export(.solution) @@ -31,6 +33,9 @@ export(grade_this_table) export(grade_this_vector) export(is_problem) export(is_tblcheck_problem) +export(problem) +export(problem_grade) +export(problem_message) export(problem_type) export(tbl_check) export(tbl_check_class) @@ -49,6 +54,7 @@ export(tbl_grade_is_table) export(tbl_grade_names) export(tbl_grade_table) export(tblcheck_grade) +export(tblcheck_message) export(vec_check) export(vec_check_class) export(vec_check_dimensions) diff --git a/NEWS.md b/NEWS.md index 5085ce48..5ed67985 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,12 @@ - Messages now only suggest using the functions `group_by()`, `ungroup()` and `rowwise()` if both `object` and `expected` are `data.frame`s (#119). - Messages now describe non-atomic variables with length > 1 as "objects", not "vectors" (#122). +* `problem()` and `problem_message()` are now exported functions. The first helps other packages create problem objects and the second is a generic method that can be used to turn a problem object into a human readable description (#124). + +## Deprecated Functions + +* `tblcheck_grade()` is now called `problem_grade()`. The old function name will continue to work, but a deprecation warning will prompt you to update to `problem_grade()` (#124). + # tblcheck 0.1.3 * Add `tolerance` argument to `vec_*_values()` (#111). diff --git a/R/assert.R b/R/assert.R index 3307bf52..dea322de 100644 --- a/R/assert.R +++ b/R/assert.R @@ -12,7 +12,7 @@ return_if_internal_problem <- function(expr, ..., env = parent.frame()) { } #' @export -tblcheck_grade.tblcheck_internal_problem <- function( +problem_grade.tblcheck_internal_problem <- function( problem, max_diffs = 3, env = parent.frame(), ... ) { # move error up to top-level of grade diff --git a/R/check_class.R b/R/check_class.R index e300a4d2..80e7a280 100644 --- a/R/check_class.R +++ b/R/check_class.R @@ -101,7 +101,7 @@ tbl_check_class <- function( exp_class, obj_class, # Object lengths are stored so the correct pluralization - # can be applied in tblcheck_message.class_problem() + # can be applied in problem_message.class_problem() expected_length = length(expected), actual_length = length(object) ) @@ -121,7 +121,7 @@ tbl_grade_class <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check_class(object, expected, ignore_class, env), env = env, ... @@ -133,7 +133,7 @@ tbl_grade_class <- function( vec_grade_class <- tbl_grade_class #' @export -tblcheck_message.class_problem <- function(problem, ...) { +problem_message.class_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$msg <- problem$msg %||% "Your `{column}` column should be {expected}, but it is {actual}." diff --git a/R/check_column.R b/R/check_column.R index 60ed83bc..e9b9487a 100644 --- a/R/check_column.R +++ b/R/check_column.R @@ -140,7 +140,7 @@ tbl_grade_column <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check_column( column = column, object = object, diff --git a/R/check_dimensions.R b/R/check_dimensions.R index 06be3ad7..11f88447 100644 --- a/R/check_dimensions.R +++ b/R/check_dimensions.R @@ -113,7 +113,7 @@ tbl_grade_dimensions <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check_dimensions(object, expected, check_ncol = check_ncol, env = env), env = env, ... @@ -129,7 +129,7 @@ vec_grade_dimensions <- tbl_grade_dimensions vec_grade_length <- tbl_grade_dimensions #' @export -tblcheck_message.dimensions_n_problem <- function(problem, ...) { +problem_message.dimensions_n_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$exp_msg <- problem$exp_msg %||% ngettext( @@ -164,7 +164,7 @@ tblcheck_message.dimensions_n_problem <- function(problem, ...) { } #' @export -tblcheck_message.length_problem <- function(problem, ...) { +problem_message.length_problem <- function(problem, ...) { problem$value_msg <- "" if (is_problem(problem, "column")) { @@ -225,7 +225,7 @@ tblcheck_message.length_problem <- function(problem, ...) { } #' @export -tblcheck_message.ncol_problem <- function(problem, ...) { +problem_message.ncol_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$exp_msg <- problem$exp_msg %||% ngettext( @@ -260,7 +260,7 @@ tblcheck_message.ncol_problem <- function(problem, ...) { } #' @export -tblcheck_message.nrow_problem <- function(problem, ...) { +problem_message.nrow_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$exp_msg <- problem$exp_msg %||% ngettext( @@ -295,7 +295,7 @@ tblcheck_message.nrow_problem <- function(problem, ...) { } #' @export -tblcheck_message.dimensions_problem <- function(problem, ...) { +problem_message.dimensions_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$msg <- problem$exp_msg %||% gettext("Your `{column}` column should be an array with dimensions {expected}, but it has dimensions {actual}.") diff --git a/R/check_groups.R b/R/check_groups.R index 6f6a32c5..323f3ce5 100644 --- a/R/check_groups.R +++ b/R/check_groups.R @@ -69,7 +69,7 @@ tbl_grade_groups <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check_groups(object, expected, env = env), max_diffs = max_diffs, env = env, @@ -78,7 +78,7 @@ tbl_grade_groups <- function( } #' @export -tblcheck_message.groups_problem <- function(problem, max_diffs = 3, ...) { +problem_message.groups_problem <- function(problem, max_diffs = 3, ...) { if (is_problem(problem, "table")) { problem$missing_msg <- problem$missing_msg %||% gettext("Your table should be grouped by {missing}. ") diff --git a/R/check_is_table.R b/R/check_is_table.R index a4f51e86..0e1412c9 100644 --- a/R/check_is_table.R +++ b/R/check_is_table.R @@ -55,7 +55,7 @@ tbl_grade_is_table <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check_is_table(object, env), env = env, ... @@ -63,7 +63,7 @@ tbl_grade_is_table <- function( } #' @export -tblcheck_message.not_table_problem <- function(problem, ...) { +problem_message.not_table_problem <- function(problem, ...) { problem$msg <- problem$msg %||% "Your result should be a table, but it is {actual}." diff --git a/R/check_levels.R b/R/check_levels.R index 7e904755..ee49556c 100644 --- a/R/check_levels.R +++ b/R/check_levels.R @@ -87,7 +87,7 @@ vec_grade_levels <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( vec_check_levels(object, expected, env = env), max_diffs = max_diffs, env = env, @@ -96,7 +96,7 @@ vec_grade_levels <- function( } #' @export -tblcheck_message.levels_problem <- function(problem, max_diffs = 3, ...) { +problem_message.levels_problem <- function(problem, max_diffs = 3, ...) { if (is_problem(problem, "column")) { problem$missing_msg <- problem$missing_msg %||% ngettext( @@ -143,7 +143,7 @@ tblcheck_message.levels_problem <- function(problem, max_diffs = 3, ...) { } #' @export -tblcheck_message.levels_n_problem <- function(problem, ...) { +problem_message.levels_n_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$exp_msg <- problem$exp_msg %||% ngettext( @@ -171,7 +171,7 @@ tblcheck_message.levels_n_problem <- function(problem, ...) { } #' @export -tblcheck_message.levels_reversed_problem <- function(problem, ...) { +problem_message.levels_reversed_problem <- function(problem, ...) { if (is_problem(problem, "column")) { problem$msg <- problem$msg %||% gettext("The order of the levels in your `{column}` column are the reverse of the expected order.") @@ -184,7 +184,7 @@ tblcheck_message.levels_reversed_problem <- function(problem, ...) { } #' @export -tblcheck_message.levels_order_problem <- function(problem, max_diffs = 3, ...) { +problem_message.levels_order_problem <- function(problem, max_diffs = 3, ...) { if (is_problem(problem, "column")) { problem$msg <- problem$msg %||% "Your `{column}` column's levels were not in the expected order. " diff --git a/R/check_names.R b/R/check_names.R index 3588d576..df1f9e97 100644 --- a/R/check_names.R +++ b/R/check_names.R @@ -98,7 +98,7 @@ tbl_grade_names <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check_names(object, expected, check_order = check_order, env = env), max_diffs = max_diffs, env = env, @@ -111,7 +111,7 @@ tbl_grade_names <- function( vec_grade_names <- tbl_grade_names #' @export -tblcheck_message.names_problem <- function(problem, max_diffs = 3, ...) { +problem_message.names_problem <- function(problem, max_diffs = 3, ...) { if (is_problem(problem, "column")) { problem$missing_msg <- problem$missing_msg %||% ngettext( @@ -172,7 +172,7 @@ tblcheck_message.names_problem <- function(problem, max_diffs = 3, ...) { } #' @export -tblcheck_message.names_order_problem <- function(problem, max_diffs = 3, ...) { +problem_message.names_order_problem <- function(problem, max_diffs = 3, ...) { problem$n_values <- min( max(length(problem$expected), length(problem$actual)), max_diffs diff --git a/R/check_table.R b/R/check_table.R index 53f7adaf..7cfab0db 100644 --- a/R/check_table.R +++ b/R/check_table.R @@ -152,12 +152,12 @@ tbl_check <- function( tbl_check_class(object, expected, ignore_class), prefix = "table" ) - } else ( + } else { return_if_problem( tbl_check_is_table(object), prefix = "table" ) - ) + } # filter columns in object and expected ---- cols <- rlang::enexpr(cols) @@ -235,7 +235,7 @@ tbl_grade <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( tbl_check( object = object, expected = expected, diff --git a/R/check_values.R b/R/check_values.R index 4db7d8a7..c8478d16 100644 --- a/R/check_values.R +++ b/R/check_values.R @@ -87,7 +87,7 @@ vec_grade_values <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( vec_check_values( object = object, expected = expected, @@ -101,7 +101,7 @@ vec_grade_values <- function( } #' @export -tblcheck_message.values_problem <- function(problem, max_diffs = 3, ...) { +problem_message.values_problem <- function(problem, max_diffs = 3, ...) { # If values problem is empty, return vague message if (is.null(problem$actual) && is.null(problem$expected)) { if (is_problem(problem, "column")) { @@ -215,5 +215,5 @@ tblcheck_message.values_problem <- function(problem, max_diffs = 3, ...) { } # If all else fails, return vague message - tblcheck_message(problem("values")) + problem_message(problem("values")) } diff --git a/R/check_vector.R b/R/check_vector.R index 800002ce..39d790b1 100644 --- a/R/check_vector.R +++ b/R/check_vector.R @@ -149,7 +149,7 @@ vec_grade <- function( env = parent.frame(), ... ) { - tblcheck_grade( + problem_grade( vec_check( object = object, expected = expected, diff --git a/R/deprecated.R b/R/deprecated.R index 75558710..080dee4f 100644 --- a/R/deprecated.R +++ b/R/deprecated.R @@ -134,3 +134,25 @@ vec_grade_vector <- function( ... ) } + +#' Deprecated Generics +#' +#' These generics are now deprecated. +#' +#' @keywords internal +#' @name deprecated-methods +NULL + +#' @describeIn deprecated-methods is now [problem_grade()]. +#' @export +tblcheck_grade <- function(problem, ...) { + lifecycle::deprecate_soft("0.2.0", "tblcheck_grade()", "problem_grade()") + problem_grade(problem, ...) +} + +#' @describeIn deprecated-methods is now [problem_message()]. +#' @export +tblcheck_message <- function(problem, ...) { + lifecycle::deprecate_soft("0.2.0", "tblcheck_message()", "problem_message()") + problem_message(problem, ...) +} diff --git a/R/grade.R b/R/grade.R deleted file mode 100644 index 62253cb7..00000000 --- a/R/grade.R +++ /dev/null @@ -1,95 +0,0 @@ -#' Apply automatic grading to a problem object -#' -#' @param problem A problem generated by a `tbl_check_*()` function. -#' @inheritParams tbl_check -#' @param env The environment used for grading. -#' @inheritDotParams gradethis::fail -message -#' -#' @return A [gradethis::fail()] message or `NULL` invisibly. -#' @export -#' -#' @examples -#' .result <- 1:10 -#' .solution <- letters[1:10] -#' problem <- vec_check() -#' tblcheck_grade(problem) -tblcheck_grade <- function(problem, max_diffs = 3, env = parent.frame(), ...) { - UseMethod("tblcheck_grade") -} - -#' @rdname tblcheck_grade -#' @export -tblcheck_grade.default <- function( - problem, max_diffs = 3, env = parent.frame(), ... -) { - invisible() -} - -#' @rdname tblcheck_grade -#' @export -tblcheck_grade.list <- function( - problem, max_diffs = 3, env = parent.frame(), ... -) { - problem <- as_problem(problem) - tblcheck_grade(problem, max_diffs = max_diffs, env = env, ...) -} - -#' @rdname tblcheck_grade -#' @export -tblcheck_grade.tblcheck_problem <- function( - problem, max_diffs = 3, env = parent.frame(), ... -) { - if (is.null(problem)) { - return(invisible()) - } - - err <- catch_internal_problem( - checkmate::assert_number(max_diffs, lower = 1), - call = find_tblcheck_call() - ) - - if (is_problem(err)) { - return(tblcheck_grade(err)) - } - - gradethis::fail( - tblcheck_message(problem, max_diffs = max_diffs), - problem = problem, - env = env, - ... - ) -} - -tblcheck_message <- function(problem, ...) { - UseMethod("tblcheck_message") -} - -#' @export -tblcheck_message.default <- function(problem, ...) { - invisible() -} - -#' @export -tblcheck_message.tblcheck_problem <- function(problem, ...) { - type_msg <- if (!is.null(problem$type)) { - gettext("Your code resulted in a `{type}` problem. ") - } else { - "" - } - - exp_msg <- if (!is.null(problem$expected)) { - expected <- paste(md_code(problem$expected), collapse = ", ") - gettext("I was expecting a value of {expected}. ") - } else { - "" - } - - obj_msg <- if (!is.null(problem$actual)) { - actual <- paste(md_code(problem$actual), collapse = ", ") - gettext("Your result gave a value of `{actual}`. ") - } else { - "" - } - - glue::glue_data(problem, type_msg, exp_msg, obj_msg) -} diff --git a/R/problem.R b/R/problem.R index 961cc653..f9388ed0 100644 --- a/R/problem.R +++ b/R/problem.R @@ -3,19 +3,44 @@ #' Useful for constructing a small list to communicate the problem that was #' discovered during checking. #' +#' @examples +#' problem( +#' type = "class", +#' expected = "character", +#' actual = "numeric", +#' expected_length = 1, +#' actual_length = 2 +#' ) +#' #' @param type A character string, e.g. `column_values` or `table_rows`, that #' describes the problem that was discovered. #' @param expected,actual The expected and actual values. These should be #' included when the value is a summary, e.g. `nrow(expected)` or #' `length(actual)`. Be careful not to include large amounts of data. #' @param ... Additional elements to be included in the `problem` object. +#' @param .class The class of the problem. Typically, we expect the problem +#' class to be `_problem`, but if you are building custom classes you +#' may set these classes as desired. +#' +#' @return Returns a problem with class `_problem` and the base classes +#' `tblcheck_problem` and `gradethis_problem`. #' -#' @keywords internal -#' @noRd +#' @family Problem functions +#' @export problem <- function( - type, expected = NULL, actual = NULL, ... + type, + expected = NULL, + actual = NULL, + ..., + .class = c(paste0(type, "_problem"), "tblcheck_problem") ) { checkmate::assert_string(type, min.chars = 1) + if (!checkmate::test_character(.class, pattern = "^[[:alpha:]][[:alnum:]_.]*$")) { + rlang::abort( + "`.class` must be a character vector of valid R class names", + class = "error_problem_class" + ) + } problem <- list( type = type, @@ -26,7 +51,7 @@ problem <- function( structure( purrr::compact(problem), - class = c(paste0(type, "_problem"), "tblcheck_problem", "gradethis_problem") + class = unique(c(.class, "gradethis_problem")) ) } @@ -64,6 +89,11 @@ return_if_problem <- function( #' If `type` is specified, `is_problem()` and `is_tblcheck_problem()` test #' whether an object is a problem of the specified type. #' +#' @examples +#' problem_type(vec_check(1, "1")) +#' is_problem(vec_check(1, "1"), "vector_class") +#' is_tblcheck_problem(vec_check(1, "1"), "class") +#' #' @param x An object #' @param type `[character(1)]`\cr A `problem` type #' @@ -71,12 +101,9 @@ return_if_problem <- function( #' of length 1. #' `problem_type()` returns a [character] of length 1. #' `as_problem()` returns a `tblcheck_problem`. -#' @export #' -#' @examples -#' problem_type(vec_check(1, "1")) -#' is_problem(vec_check(1, "1"), "vector_class") -#' is_tblcheck_problem(vec_check(1, "1"), "class") +#' @family Problem functions +#' @export problem_type <- function(x) { if (is_problem(x)) { return(x$type) @@ -88,34 +115,43 @@ problem_type <- function(x) { #' @rdname problem_type #' @export is_problem <- function(x, type = NULL) { - inherits(x, "gradethis_problem") && ( - is.null(type) || inherits(x, paste0(type, "_problem")) - ) + if (!inherits(x, "gradethis_problem")) return(FALSE) + if (is.null(type)) return(TRUE) + inherits(x, c(type, paste0(type, "_problem"))) } #' @rdname problem_type #' @export is_tblcheck_problem <- function(x, type = NULL) { - inherits(x, "tblcheck_problem") && ( - is.null(type) || inherits(x, paste0(type, "_problem")) - ) + if (!inherits(x, "tblcheck_problem")) return(FALSE) + if (is.null(type)) return(TRUE) + # tblcheck problem classes always are "_problem" + inherits(x, paste0(type, "_problem")) } #' @rdname problem_type #' @export as_problem <- function(x) { checkmate::assert_list(x) - class(x) <- c("tblcheck_problem", "gradethis_problem") - if (!is.null(x$location)) { - class(x) <- c(paste0(x$location, "_problem"), class(x)) + if (!is.null(x$location) && !is.null(x$type) && is.null(x$.class)) { + # this is probably a tblcheck problem as a list + x$.class <- c( + paste0(c(x$type, x$location), "_problem"), + "tblcheck_problem", + "gradethis_problem" + ) } - if (!is.null(problem_type(x))) { - class(x) <- c(paste0(problem_type(x), "_problem"), class(x)) - } - - x + tryCatch( + rlang::eval_bare(rlang::call2("problem", !!!x)), + error_problem_class = function(err) { + rlang::abort( + "Please set `.class` for your list, see `?problem()` for details", + parent = err + ) + } + ) } #' @export @@ -127,5 +163,5 @@ print.tblcheck_problem <- function(x, ...) { #' @export format.tblcheck_problem <- function(x, ...) { - tblcheck_message(x, ...) + problem_message(x, ...) } diff --git a/R/problem_grade.R b/R/problem_grade.R new file mode 100644 index 00000000..32e94037 --- /dev/null +++ b/R/problem_grade.R @@ -0,0 +1,79 @@ +#' Apply automatic grading to a problem object +#' +#' Automatically converts a [problem()] object into a \pkg{gradethis} grade. +#' `problem_grade()` is an S4 generic and \pkg{tblcheck} provides an internal +#' method for problems with class `"tblcheck_problem"`. In \pkg{tblcheck}, or +#' for problems with this class, any problems are automatically turned into +#' failing grades with [gradethis::fail()] and using the message provided by +#' [problem_message()]. +#' +#' @examples +#' .result <- 1:10 +#' .solution <- letters[1:10] +#' problem <- vec_check() +#' problem_grade(problem) +#' +#' @param problem A problem generated by [tbl_check()], [vec_check()] or their +#' related helper functions. +#' @inheritParams tbl_check +#' @param env The environment used for grading. +#' @inheritDotParams gradethis::fail -message +#' +#' @return A [gradethis::fail()] message or `NULL` invisibly. +#' +#' @family Problem functions +#' @export +problem_grade <- function(problem, max_diffs = 3, env = parent.frame(), ...) { + UseMethod("problem_grade") +} + +#' @rdname problem_grade +#' @export +problem_grade.default <- function( + problem, max_diffs = 3, env = parent.frame(), ... +) { + invisible() +} + +#' @rdname problem_grade +#' @export +problem_grade.list <- function( + problem, max_diffs = 3, env = parent.frame(), ... +) { + problem <- as_problem(problem) + problem_grade(problem, max_diffs = max_diffs, env = env, ...) +} + +#' @rdname problem_grade +#' @export +problem_grade.gradethis_problem <- function( + problem, max_diffs = 3, env = parent.frame(), ... +) { + if (is.null(problem)) { + return(invisible()) + } + + err <- catch_internal_problem( + checkmate::assert_number(max_diffs, lower = 1), + call = find_tblcheck_call() + ) + + if (is_problem(err)) { + return(problem_grade(err)) + } + + gradethis::fail( + problem_message(problem, max_diffs = max_diffs), + problem = problem, + env = env, + ... + ) +} + +#' @rdname problem_grade +#' @export +problem_grade.tblcheck_problem <- function( + problem, max_diffs = 3, env = parent.frame(), ... +) { + NextMethod() +} diff --git a/R/problem_message.R b/R/problem_message.R new file mode 100644 index 00000000..b8ded33b --- /dev/null +++ b/R/problem_message.R @@ -0,0 +1,64 @@ +#' Create a message from a problem object +#' +#' `problem_message()` is an S3 generic that powers the conversion of problems +#' detected by [tbl_check()], [vec_check()], and their related helper functions +#' into a human-readable message. +#' +#' @examples +#' problem <- problem( +#' type = "class", +#' expected = "character", +#' actual = "numeric", +#' expected_length = 1, +#' actual_length = 2 +#' ) +#' +#' problem_message(problem) +#' +#' @param problem An object with base class `gradethis_problem`. Problems +#' identified by \pkg{tblcheck} also include `tblcheck_problem`, plus +#' additional classes that more specifically identify the problem type. +#' @param ... Additional arguments passed to the underlying methods. +#' +#' @return A length-1 character string with a message describing the problem. +#' +#' @family Problem functions +#' @export +problem_message <- function(problem, ...) { + UseMethod("problem_message") +} + +#' @export +problem_message.default <- function(problem, ...) { + invisible() +} + +#' @export +problem_message.gradethis_problem <- function(problem, ...) { + type_msg <- if (!is.null(problem$type)) { + gettext("Your code resulted in a `{type}` problem. ") + } else { + "" + } + + exp_msg <- if (!is.null(problem$expected)) { + expected <- paste(md_code(problem$expected), collapse = ", ") + gettext("I was expecting a value of {expected}. ") + } else { + "" + } + + obj_msg <- if (!is.null(problem$actual)) { + actual <- paste(md_code(problem$actual), collapse = ", ") + gettext("Your result gave a value of `{actual}`. ") + } else { + "" + } + + glue::glue_data(problem, type_msg, exp_msg, obj_msg) +} + +#' @export +problem_message.tblcheck_problem <- function(problem, ...) { + NextMethod() +} diff --git a/man/deprecated-methods.Rd b/man/deprecated-methods.Rd new file mode 100644 index 00000000..64a97fae --- /dev/null +++ b/man/deprecated-methods.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/deprecated.R +\name{deprecated-methods} +\alias{deprecated-methods} +\alias{tblcheck_grade} +\alias{tblcheck_message} +\title{Deprecated Generics} +\usage{ +tblcheck_grade(problem, ...) + +tblcheck_message(problem, ...) +} +\description{ +These generics are now deprecated. +} +\section{Functions}{ +\itemize{ +\item \code{tblcheck_grade()}: is now \code{\link[=problem_grade]{problem_grade()}}. + +\item \code{tblcheck_message()}: is now \code{\link[=problem_message]{problem_message()}}. + +}} +\keyword{internal} diff --git a/man/grade_this_table.Rd b/man/grade_this_table.Rd index 5a472ce4..2477f3af 100644 --- a/man/grade_this_table.Rd +++ b/man/grade_this_table.Rd @@ -85,7 +85,7 @@ with \code{\link[=tbl_check_dimensions]{tbl_check_dimensions()}}.} \item{check_groups}{\verb{[logical(1)]}\cr Whether to check that \code{object} and \code{expected} have the same \link[dplyr:group_by]{groups} -with \code{\link[dplyr:group_data]{dplyr::group_vars()}}.} +with \code{\link[dplyr:group_vars]{dplyr::group_vars()}}.} \item{check_columns}{\verb{[logical(1)]}\cr Whether to check that all columns have the same contents with \code{\link[=tbl_check_column]{tbl_check_column()}}.} diff --git a/man/problem.Rd b/man/problem.Rd new file mode 100644 index 00000000..37d6da56 --- /dev/null +++ b/man/problem.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/problem.R +\name{problem} +\alias{problem} +\title{Declare a problem} +\usage{ +problem( + type, + expected = NULL, + actual = NULL, + ..., + .class = c(paste0(type, "_problem"), "tblcheck_problem") +) +} +\arguments{ +\item{type}{A character string, e.g. \code{column_values} or \code{table_rows}, that +describes the problem that was discovered.} + +\item{expected, actual}{The expected and actual values. These should be +included when the value is a summary, e.g. \code{nrow(expected)} or +\code{length(actual)}. Be careful not to include large amounts of data.} + +\item{...}{Additional elements to be included in the \code{problem} object.} + +\item{.class}{The class of the problem. Typically, we expect the problem +class to be \verb{_problem}, but if you are building custom classes you +may set these classes as desired.} +} +\value{ +Returns a problem with class \verb{_problem} and the base classes +\code{tblcheck_problem} and \code{gradethis_problem}. +} +\description{ +Useful for constructing a small list to communicate the problem that was +discovered during checking. +} +\examples{ +problem( + type = "class", + expected = "character", + actual = "numeric", + expected_length = 1, + actual_length = 2 +) + +} +\seealso{ +Other Problem functions: +\code{\link{problem_grade}()}, +\code{\link{problem_message}()}, +\code{\link{problem_type}()} +} +\concept{Problem functions} diff --git a/man/tblcheck_grade.Rd b/man/problem_grade.Rd similarity index 54% rename from man/tblcheck_grade.Rd rename to man/problem_grade.Rd index aec7b299..3c3d20ae 100644 --- a/man/tblcheck_grade.Rd +++ b/man/problem_grade.Rd @@ -1,22 +1,26 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/grade.R -\name{tblcheck_grade} -\alias{tblcheck_grade} -\alias{tblcheck_grade.default} -\alias{tblcheck_grade.list} -\alias{tblcheck_grade.tblcheck_problem} +% Please edit documentation in R/problem_grade.R +\name{problem_grade} +\alias{problem_grade} +\alias{problem_grade.default} +\alias{problem_grade.list} +\alias{problem_grade.gradethis_problem} +\alias{problem_grade.tblcheck_problem} \title{Apply automatic grading to a problem object} \usage{ -tblcheck_grade(problem, max_diffs = 3, env = parent.frame(), ...) +problem_grade(problem, max_diffs = 3, env = parent.frame(), ...) -\method{tblcheck_grade}{default}(problem, max_diffs = 3, env = parent.frame(), ...) +\method{problem_grade}{default}(problem, max_diffs = 3, env = parent.frame(), ...) -\method{tblcheck_grade}{list}(problem, max_diffs = 3, env = parent.frame(), ...) +\method{problem_grade}{list}(problem, max_diffs = 3, env = parent.frame(), ...) -\method{tblcheck_grade}{tblcheck_problem}(problem, max_diffs = 3, env = parent.frame(), ...) +\method{problem_grade}{gradethis_problem}(problem, max_diffs = 3, env = parent.frame(), ...) + +\method{problem_grade}{tblcheck_problem}(problem, max_diffs = 3, env = parent.frame(), ...) } \arguments{ -\item{problem}{A problem generated by a \verb{tbl_check_*()} function.} +\item{problem}{A problem generated by \code{\link[=tbl_check]{tbl_check()}}, \code{\link[=vec_check]{vec_check()}} or their +related helper functions.} \item{max_diffs}{\verb{[numeric(1)]}\cr The maximum number of mismatched values to display in an informative failure message. @@ -44,11 +48,24 @@ using \code{\link[gradethis:gradethis_setup]{gradethis_setup()}} or the \code{gr A \code{\link[gradethis:graded]{gradethis::fail()}} message or \code{NULL} invisibly. } \description{ -Apply automatic grading to a problem object +Automatically converts a \code{\link[=problem]{problem()}} object into a \pkg{gradethis} grade. +\code{problem_grade()} is an S4 generic and \pkg{tblcheck} provides an internal +method for problems with class \code{"tblcheck_problem"}. In \pkg{tblcheck}, or +for problems with this class, any problems are automatically turned into +failing grades with \code{\link[gradethis:graded]{gradethis::fail()}} and using the message provided by +\code{\link[=problem_message]{problem_message()}}. } \examples{ .result <- 1:10 .solution <- letters[1:10] problem <- vec_check() -tblcheck_grade(problem) +problem_grade(problem) + +} +\seealso{ +Other Problem functions: +\code{\link{problem_message}()}, +\code{\link{problem_type}()}, +\code{\link{problem}()} } +\concept{Problem functions} diff --git a/man/problem_message.Rd b/man/problem_message.Rd new file mode 100644 index 00000000..8fcec286 --- /dev/null +++ b/man/problem_message.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/problem_message.R +\name{problem_message} +\alias{problem_message} +\title{Create a message from a problem object} +\usage{ +problem_message(problem, ...) +} +\arguments{ +\item{problem}{An object with base class \code{gradethis_problem}. Problems +identified by \pkg{tblcheck} also include \code{tblcheck_problem}, plus +additional classes that more specifically identify the problem type.} + +\item{...}{Additional arguments passed to the underlying methods.} +} +\value{ +A length-1 character string with a message describing the problem. +} +\description{ +\code{problem_message()} is an S3 generic that powers the conversion of problems +detected by \code{\link[=tbl_check]{tbl_check()}}, \code{\link[=vec_check]{vec_check()}}, and their related helper functions +into a human-readable message. +} +\examples{ +problem <- problem( + type = "class", + expected = "character", + actual = "numeric", + expected_length = 1, + actual_length = 2 +) + +problem_message(problem) + +} +\seealso{ +Other Problem functions: +\code{\link{problem_grade}()}, +\code{\link{problem_type}()}, +\code{\link{problem}()} +} +\concept{Problem functions} diff --git a/man/problem_type.Rd b/man/problem_type.Rd index d596e23c..208e4b64 100644 --- a/man/problem_type.Rd +++ b/man/problem_type.Rd @@ -44,4 +44,12 @@ whether an object is a problem of the specified type. problem_type(vec_check(1, "1")) is_problem(vec_check(1, "1"), "vector_class") is_tblcheck_problem(vec_check(1, "1"), "class") + +} +\seealso{ +Other Problem functions: +\code{\link{problem_grade}()}, +\code{\link{problem_message}()}, +\code{\link{problem}()} } +\concept{Problem functions} diff --git a/man/tbl_check.Rd b/man/tbl_check.Rd index ce1ac3e6..2de59ce3 100644 --- a/man/tbl_check.Rd +++ b/man/tbl_check.Rd @@ -78,7 +78,7 @@ with \code{\link[=tbl_check_dimensions]{tbl_check_dimensions()}}.} \item{check_groups}{\verb{[logical(1)]}\cr Whether to check that \code{object} and \code{expected} have the same \link[dplyr:group_by]{groups} -with \code{\link[dplyr:group_data]{dplyr::group_vars()}}.} +with \code{\link[dplyr:group_vars]{dplyr::group_vars()}}.} \item{check_columns}{\verb{[logical(1)]}\cr Whether to check that all columns have the same contents with \code{\link[=tbl_check_column]{tbl_check_column()}}.} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 31f19d80..d0d389b5 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -96,5 +96,4 @@ reference: Inspect the contents of a `problem` from a check function or transform a `problem` into a grade. contents: - - '`tblcheck_grade`' - - '`problem_type`' + - starts_with("problem") diff --git a/tests/testthat/test-grade.R b/tests/testthat/test-grade.R index 314a599f..8b4c0ae2 100644 --- a/tests/testthat/test-grade.R +++ b/tests/testthat/test-grade.R @@ -3,7 +3,7 @@ test_that("grading", { .solution <- tibble::tibble(a = letters, b = a) tbl_grade <- tbl_grade() - problem_grade <- tblcheck_grade(tbl_check()) + problem_grade <- problem_grade(tbl_check()) expect_equal(tbl_grade, problem_grade) }) @@ -12,8 +12,8 @@ test_that("list grading", { .result <- tibble::tibble(a = letters, b = a, c = a) .solution <- tibble::tibble(a = letters, b = a) - problem_grade <- tblcheck_grade(tbl_check()) - list_grade <- tblcheck_grade(unclass(tbl_check())) + problem <- tbl_check() + problem_list <- unclass(problem) - expect_equal(problem_grade, list_grade) + expect_equal(problem_grade(problem), problem_grade(problem_list)) }) diff --git a/vignettes/tblcheck.Rmd b/vignettes/tblcheck.Rmd index 049ace1e..2c0d3adf 100644 --- a/vignettes/tblcheck.Rmd +++ b/vignettes/tblcheck.Rmd @@ -804,12 +804,12 @@ if (is_problem(problem, "values") && all.equal(problem$actual, c(3, 5))) { ``` For problems not handled by your custom grading code, you can pass the problem to `tbl_grade()` to create a grade with the default feedback provided by tblcheck's `grade` functions. -If there are no problems, `tblcheck_grade(problem)` won't return anything. +If there are no problems, `problem_grade(problem)` won't return anything. Here's the default feedback `tbl_grade_column()` _would have returned_ without our custom grading code. ```{r custom-problem-ex-fallback} -tblcheck_grade(problem) +problem_grade(problem) ``` **Tip**: You can also use `if` statements to ignore differences that you don't care about in your grading code. @@ -829,7 +829,7 @@ if (is_problem(problem, "values") && all.equal(problem$actual, c(3, 5))) { fail(feedback) } -tblcheck_grade(problem) +problem_grade(problem) pass("Great job!") ``` ````