From 54c5a86e6ee1fa3125352b8413d95cb6da43f43a Mon Sep 17 00:00:00 2001 From: selkamand <73202525+selkamand@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:08:50 +1100 Subject: [PATCH 1/6] Refine assert_function_expects tests --- NAMESPACE | 1 + NEWS.md | 2 ++ R/assert_functions.R | 33 ++++++++++++++++++++++ R/utils.R | 10 +++++++ man/assert_function_expects.Rd | 31 +++++++++++++++++++++ tests/testthat/test-assert_functions.R | 38 ++++++++++++++++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 man/assert_function_expects.Rd diff --git a/NAMESPACE b/NAMESPACE index 8f8c406..59645e1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -31,6 +31,7 @@ export(assert_file_has_extension) export(assert_finite) export(assert_flag) export(assert_function) +export(assert_function_expects) export(assert_function_expects_n_arguments) export(assert_greater_than) export(assert_greater_than_or_equal_to) diff --git a/NEWS.md b/NEWS.md index da0a03c..3761704 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # assertions 0.2.0 +* Added `assert_function_expects()` for checking required argument names in functions + * Added `assert_all_strings_contain()` and `assert_string_contains()` for checking character inputs against regular expressions * Added `assert_packages_installed()` diff --git a/R/assert_functions.R b/R/assert_functions.R index 6ae3df0..519bb40 100644 --- a/R/assert_functions.R +++ b/R/assert_functions.R @@ -24,6 +24,24 @@ function_expects_n_arguments_advanced <- function(x, n, dots = c("throw_error"," return(TRUE) } +function_expects_advanced <- function(x, required){ + if(!is.function(x)) + return("{.strong '{arg_name}'} must be a function, not a {class(arg_value)}") + + required_args <- func_required_arg_names(x) + if(!is_subset(required, required_args)){ + missing_args <- setopts_exlusive_to_first(required, required_args) + missing_count <- length(missing_args) + missing_args <- paste0("`", paste(missing_args, collapse = "`, `"), "`") + return(paste0("'{.strong {arg_name}}' is missing {.strong ", missing_count, "} required argument", + if(missing_count == 1) "" else "s", + ": {.strong ", missing_args, "}" + )) + } + + return(TRUE) +} + # Assertions -------------------------------------------------------------- @@ -42,3 +60,18 @@ function_expects_n_arguments_advanced <- function(x, n, dots = c("throw_error"," #' #' @export assert_function_expects_n_arguments <- assert_create(func = function_expects_n_arguments_advanced) + +#' Assert function expects required argument names +#' +#' Assert a function expects a set of required argument names. +#' +#' @include assert_create.R +#' @include utils.R +#' @param x a function to check includes required argument names +#' @param required a character vector of required argument names +#' @inheritParams common_roxygen_params +#' +#' @return invisible(TRUE) if function `x` expects all required arguments, otherwise aborts with the error message specified by `msg` +#' +#' @export +assert_function_expects <- assert_create(func = function_expects_advanced) diff --git a/R/utils.R b/R/utils.R index 40af336..41f84e5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -68,6 +68,16 @@ func_supports_variable_arguments <- function(func){ func_args_as_pairlist <- function(func){ formals(args(func)) } + +func_required_arg_names <- function(func){ + args <- formals(args(func)) + if(length(args) == 0){ + return(character(0)) + } + required_args <- args[vapply(args, is.symbol, FUN.VALUE = TRUE)] + required_args <- names(required_args) + setdiff(required_args, "...") +} # # func_args_as_alist <- function(func){ # a= unlist(func_args_as_pairlist(func)) diff --git a/man/assert_function_expects.Rd b/man/assert_function_expects.Rd new file mode 100644 index 0000000..6446732 --- /dev/null +++ b/man/assert_function_expects.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assert_functions.R +\name{assert_function_expects} +\alias{assert_function_expects} +\title{Assert function expects required argument names} +\usage{ +assert_function_expects( + x, + required, + msg = NULL, + call = rlang::caller_env(), + arg_name = NULL +) +} +\arguments{ +\item{x}{a function to check includes required argument names} + +\item{required}{a character vector of required argument names} + +\item{msg}{The error message thrown if the assertion fails (string)} + +\item{call}{Only relevant when pooling assertions into multi-assertion helper functions. See \link[cli]{cli_abort} for details.} + +\item{arg_name}{Advanced use only. Name of the argument passed (default: NULL, will automatically extract arg_name).} +} +\value{ +invisible(TRUE) if function \code{x} expects all required arguments, otherwise aborts with the error message specified by \code{msg} +} +\description{ +Assert a function expects a set of required argument names. +} diff --git a/tests/testthat/test-assert_functions.R b/tests/testthat/test-assert_functions.R index 3f91c31..f8513be 100644 --- a/tests/testthat/test-assert_functions.R +++ b/tests/testthat/test-assert_functions.R @@ -6,6 +6,9 @@ fn_1_arg <- function(a) {} fn_2_args <- function(a, b) {} fn_1_arg_with_dots <- function(a, ...) {} fn_2_args_with_dots <- function(a, b, ...) {} +fn_with_required_args <- function(x, y, z = 1, ...) {} +fn_with_defaults <- function(x = 1, y = 2) {} +fn_with_no_required <- function(...) {} # Unit tests for `function_expects_n_arguments_advanced` test_that("function_expects_n_arguments_advanced behaves correctly for exact argument count", { @@ -69,6 +72,22 @@ test_that("function_expects_n_arguments_advanced handles `dots` parameter correc expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, Inf, dots = "count_as_inf")) }) +test_that("function_expects_advanced validates required argument names", { + expect_true(function_expects_advanced(fn_2_args, c("a", "b"))) + expect_true(function_expects_advanced(fn_with_required_args, c("x", "y"))) + + expect_match(function_expects_advanced(fn_2_args, "c"), + "is missing {.strong 1} required argument", fixed = TRUE) + expect_match(function_expects_advanced(fn_2_args, c("b", "c")), + "is missing {.strong 1} required argument", fixed = TRUE) + expect_match(function_expects_advanced(fn_with_no_required, "x"), + "is missing {.strong 1} required argument", fixed = TRUE) + expect_match(function_expects_advanced(fn_with_defaults, "x"), + "is missing {.strong 1} required argument", fixed = TRUE) + expect_match(function_expects_advanced(1, "x"), + "must be a function, not a", fixed = TRUE) +}) + @@ -105,3 +124,22 @@ cli::test_that_cli("assert_function_expects_n_arguments() works", config = "plai # Custom error messages work expect_error(assert_function_expects_n_arguments(my_func, 3, msg = "Custom error message"), "Custom error message") }) + +cli::test_that_cli("assert_function_expects() works", config = "plain", { + my_func <- function(a, b, c = 1) { a + b + c } + my_func_dots <- function(a, b, ...) { a + b } + expect_true(assert_function_expects(my_func, c("a", "b"))) + expect_true(assert_function_expects(my_func_dots, c("a", "b"))) + + my_func2 <- function(a, c) { a + c } + expect_error( + assert_function_expects(my_func2, c("a", "b")), + "'my_func2' is missing 1 required argument: `b`" + ) + expect_error(assert_function_expects(123, "a"), "'123' must be a function, not a numeric") + expect_error(assert_function_expects(fn_with_no_required, "a"), + "'fn_with_no_required' is missing 1 required argument: `a`") + expect_error(assert_function_expects(fn_with_defaults, "x"), + "'fn_with_defaults' is missing 1 required argument: `x`") + expect_error(assert_function_expects(my_func, c("a", "d"), msg = "Custom error message"), "Custom error message") +}) From aebd57245252c9b59ff4fca3f0fcb92d19e7be07 Mon Sep 17 00:00:00 2001 From: selkamand <73202525+selkamand@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:39:13 +1100 Subject: [PATCH 2/6] Match assert_function_expects signature checks --- R/assert_functions.R | 10 +++++----- tests/testthat/test-assert_functions.R | 16 +++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/R/assert_functions.R b/R/assert_functions.R index 519bb40..8ed6c6f 100644 --- a/R/assert_functions.R +++ b/R/assert_functions.R @@ -28,14 +28,14 @@ function_expects_advanced <- function(x, required){ if(!is.function(x)) return("{.strong '{arg_name}'} must be a function, not a {class(arg_value)}") - required_args <- func_required_arg_names(x) - if(!is_subset(required, required_args)){ - missing_args <- setopts_exlusive_to_first(required, required_args) + declared_args <- setdiff(func_arg_names(x), "...") + if(!is_subset(required, declared_args)){ + missing_args <- setopts_exlusive_to_first(required, declared_args) missing_count <- length(missing_args) missing_args <- paste0("`", paste(missing_args, collapse = "`, `"), "`") - return(paste0("'{.strong {arg_name}}' is missing {.strong ", missing_count, "} required argument", + return(paste0("Function '{arg_name}' is missing the follwoing parameter", if(missing_count == 1) "" else "s", - ": {.strong ", missing_args, "}" + " in its signature: {.strong ", missing_args, "}" )) } diff --git a/tests/testthat/test-assert_functions.R b/tests/testthat/test-assert_functions.R index f8513be..13f52e1 100644 --- a/tests/testthat/test-assert_functions.R +++ b/tests/testthat/test-assert_functions.R @@ -77,13 +77,12 @@ test_that("function_expects_advanced validates required argument names", { expect_true(function_expects_advanced(fn_with_required_args, c("x", "y"))) expect_match(function_expects_advanced(fn_2_args, "c"), - "is missing {.strong 1} required argument", fixed = TRUE) + "missing the follwoing parameter", fixed = TRUE) expect_match(function_expects_advanced(fn_2_args, c("b", "c")), - "is missing {.strong 1} required argument", fixed = TRUE) + "missing the follwoing parameter", fixed = TRUE) expect_match(function_expects_advanced(fn_with_no_required, "x"), - "is missing {.strong 1} required argument", fixed = TRUE) - expect_match(function_expects_advanced(fn_with_defaults, "x"), - "is missing {.strong 1} required argument", fixed = TRUE) + "missing the follwoing parameter", fixed = TRUE) + expect_true(function_expects_advanced(fn_with_defaults, "x")) expect_match(function_expects_advanced(1, "x"), "must be a function, not a", fixed = TRUE) }) @@ -134,12 +133,11 @@ cli::test_that_cli("assert_function_expects() works", config = "plain", { my_func2 <- function(a, c) { a + c } expect_error( assert_function_expects(my_func2, c("a", "b")), - "'my_func2' is missing 1 required argument: `b`" + "Function 'my_func2' is missing the follwoing parameter in its signature: `b`" ) expect_error(assert_function_expects(123, "a"), "'123' must be a function, not a numeric") expect_error(assert_function_expects(fn_with_no_required, "a"), - "'fn_with_no_required' is missing 1 required argument: `a`") - expect_error(assert_function_expects(fn_with_defaults, "x"), - "'fn_with_defaults' is missing 1 required argument: `x`") + "Function 'fn_with_no_required' is missing the follwoing parameter in its signature: `a`") + expect_true(assert_function_expects(fn_with_defaults, "x")) expect_error(assert_function_expects(my_func, c("a", "d"), msg = "Custom error message"), "Custom error message") }) From 35b497fa1c89490ff496d72c5d2e91f107a143e4 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 8 Feb 2026 23:45:38 +1100 Subject: [PATCH 3/6] refactor: cleaned up function assertion code & improved docs --- R/assert_functions.R | 31 +++++++++++++++++--------- man/assert_function_expects.Rd | 14 +++++++----- man/function_expects_advanced.Rd | 17 ++++++++++++++ tests/testthat/test-assert_functions.R | 10 ++++----- 4 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 man/function_expects_advanced.Rd diff --git a/R/assert_functions.R b/R/assert_functions.R index 8ed6c6f..8b11f72 100644 --- a/R/assert_functions.R +++ b/R/assert_functions.R @@ -24,16 +24,25 @@ function_expects_n_arguments_advanced <- function(x, n, dots = c("throw_error"," return(TRUE) } + +#' Expect function has arguments +#' +#' @param required the names of parameters the function must support (character) +#' +#' @returns TRUE if the function contains the expected parameters, otherwise returns a string (the error message) +#' function_expects_advanced <- function(x, required){ - if(!is.function(x)) - return("{.strong '{arg_name}'} must be a function, not a {class(arg_value)}") + if(!is.function(x)) { + value_class <- toString(class(x)) + return(paste0("{.strong '{arg_name}'} must be a function, not a ", value_class)) + } declared_args <- setdiff(func_arg_names(x), "...") if(!is_subset(required, declared_args)){ missing_args <- setopts_exlusive_to_first(required, declared_args) missing_count <- length(missing_args) missing_args <- paste0("`", paste(missing_args, collapse = "`, `"), "`") - return(paste0("Function '{arg_name}' is missing the follwoing parameter", + return(paste0("Function '{arg_name}' is missing the following parameter", if(missing_count == 1) "" else "s", " in its signature: {.strong ", missing_args, "}" )) @@ -61,17 +70,19 @@ function_expects_advanced <- function(x, required){ #' @export assert_function_expects_n_arguments <- assert_create(func = function_expects_n_arguments_advanced) -#' Assert function expects required argument names +#' Assert function expects specific parameter names #' -#' Assert a function expects a set of required argument names. +#' Assert that a function signature includes required set of parameter names in its +#' formal argument list, regardless of whether those parameters have default +#' values. The `...` argument is ignored. #' -#' @include assert_create.R -#' @include utils.R -#' @param x a function to check includes required argument names -#' @param required a character vector of required argument names +#' @param x a function to check for required parameter names +#' @param required a character vector of parameter names that must appear in +#' the function signature (order does not matter) #' @inheritParams common_roxygen_params #' -#' @return invisible(TRUE) if function `x` expects all required arguments, otherwise aborts with the error message specified by `msg` +#' @return invisible(TRUE) if function `x` declares all required parameters, +#' otherwise aborts with the error message specified by `msg` #' #' @export assert_function_expects <- assert_create(func = function_expects_advanced) diff --git a/man/assert_function_expects.Rd b/man/assert_function_expects.Rd index 6446732..b3a3802 100644 --- a/man/assert_function_expects.Rd +++ b/man/assert_function_expects.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/assert_functions.R \name{assert_function_expects} \alias{assert_function_expects} -\title{Assert function expects required argument names} +\title{Assert function expects specific parameter names} \usage{ assert_function_expects( x, @@ -13,9 +13,10 @@ assert_function_expects( ) } \arguments{ -\item{x}{a function to check includes required argument names} +\item{x}{a function to check for required parameter names} -\item{required}{a character vector of required argument names} +\item{required}{a character vector of parameter names that must appear in +the function signature (order does not matter)} \item{msg}{The error message thrown if the assertion fails (string)} @@ -24,8 +25,11 @@ assert_function_expects( \item{arg_name}{Advanced use only. Name of the argument passed (default: NULL, will automatically extract arg_name).} } \value{ -invisible(TRUE) if function \code{x} expects all required arguments, otherwise aborts with the error message specified by \code{msg} +invisible(TRUE) if function \code{x} declares all required parameters, +otherwise aborts with the error message specified by \code{msg} } \description{ -Assert a function expects a set of required argument names. +Assert that a function signature includes required set of parameter names in its +formal argument list, regardless of whether those parameters have default +values. The \code{...} argument is ignored. } diff --git a/man/function_expects_advanced.Rd b/man/function_expects_advanced.Rd new file mode 100644 index 0000000..2a0ae89 --- /dev/null +++ b/man/function_expects_advanced.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assert_functions.R +\name{function_expects_advanced} +\alias{function_expects_advanced} +\title{Expect function has arguments} +\usage{ +function_expects_advanced(x, required) +} +\arguments{ +\item{required}{the names of parameters the function must support (character)} +} +\value{ +TRUE if the function contains the expected parameters, otherwise returns a string (the error message) +} +\description{ +Expect function has arguments +} diff --git a/tests/testthat/test-assert_functions.R b/tests/testthat/test-assert_functions.R index 13f52e1..9d14b68 100644 --- a/tests/testthat/test-assert_functions.R +++ b/tests/testthat/test-assert_functions.R @@ -77,11 +77,11 @@ test_that("function_expects_advanced validates required argument names", { expect_true(function_expects_advanced(fn_with_required_args, c("x", "y"))) expect_match(function_expects_advanced(fn_2_args, "c"), - "missing the follwoing parameter", fixed = TRUE) + "missing the following parameter", fixed = TRUE) expect_match(function_expects_advanced(fn_2_args, c("b", "c")), - "missing the follwoing parameter", fixed = TRUE) + "missing the following parameter", fixed = TRUE) expect_match(function_expects_advanced(fn_with_no_required, "x"), - "missing the follwoing parameter", fixed = TRUE) + "missing the following parameter", fixed = TRUE) expect_true(function_expects_advanced(fn_with_defaults, "x")) expect_match(function_expects_advanced(1, "x"), "must be a function, not a", fixed = TRUE) @@ -133,11 +133,11 @@ cli::test_that_cli("assert_function_expects() works", config = "plain", { my_func2 <- function(a, c) { a + c } expect_error( assert_function_expects(my_func2, c("a", "b")), - "Function 'my_func2' is missing the follwoing parameter in its signature: `b`" + "Function 'my_func2' is missing the following parameter in its signature: `b`" ) expect_error(assert_function_expects(123, "a"), "'123' must be a function, not a numeric") expect_error(assert_function_expects(fn_with_no_required, "a"), - "Function 'fn_with_no_required' is missing the follwoing parameter in its signature: `a`") + "Function 'fn_with_no_required' is missing the following parameter in its signature: `a`") expect_true(assert_function_expects(fn_with_defaults, "x")) expect_error(assert_function_expects(my_func, c("a", "d"), msg = "Custom error message"), "Custom error message") }) From 685b81d2f8ea7cbfe2d8c6ef6cc0f7324b8e3410 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 8 Feb 2026 23:50:47 +1100 Subject: [PATCH 4/6] docs: removed doc from unexported func --- R/assert_functions.R | 12 ++++++------ man/function_expects_advanced.Rd | 17 ----------------- 2 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 man/function_expects_advanced.Rd diff --git a/R/assert_functions.R b/R/assert_functions.R index 8b11f72..7c96aa2 100644 --- a/R/assert_functions.R +++ b/R/assert_functions.R @@ -25,12 +25,12 @@ function_expects_n_arguments_advanced <- function(x, n, dots = c("throw_error"," } -#' Expect function has arguments -#' -#' @param required the names of parameters the function must support (character) -#' -#' @returns TRUE if the function contains the expected parameters, otherwise returns a string (the error message) -#' +# Expect function has arguments +# @param +# @param required the names of parameters the function must support (character) +# +# @returns TRUE if the function contains the expected parameters, otherwise returns a string (the error message) +# function_expects_advanced <- function(x, required){ if(!is.function(x)) { value_class <- toString(class(x)) diff --git a/man/function_expects_advanced.Rd b/man/function_expects_advanced.Rd deleted file mode 100644 index 2a0ae89..0000000 --- a/man/function_expects_advanced.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/assert_functions.R -\name{function_expects_advanced} -\alias{function_expects_advanced} -\title{Expect function has arguments} -\usage{ -function_expects_advanced(x, required) -} -\arguments{ -\item{required}{the names of parameters the function must support (character)} -} -\value{ -TRUE if the function contains the expected parameters, otherwise returns a string (the error message) -} -\description{ -Expect function has arguments -} From 299cad1b37820706f4920f9a73588feb750020c9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 9 Feb 2026 00:03:17 +1100 Subject: [PATCH 5/6] feat: documented and added assert_function test --- tests/testthat/test-assert_functions.R | 40 +++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-assert_functions.R b/tests/testthat/test-assert_functions.R index 9d14b68..ab845c6 100644 --- a/tests/testthat/test-assert_functions.R +++ b/tests/testthat/test-assert_functions.R @@ -125,19 +125,51 @@ cli::test_that_cli("assert_function_expects_n_arguments() works", config = "plai }) cli::test_that_cli("assert_function_expects() works", config = "plain", { + + # Function with required args and one defaulted argument my_func <- function(a, b, c = 1) { a + b + c } + + # Function that accepts additional arguments via ... my_func_dots <- function(a, b, ...) { a + b } + + # Succeeds when required parameters are present (ignores defaults) expect_true(assert_function_expects(my_func, c("a", "b"))) + + # Succeeds when required parameters are present and ... is ignored expect_true(assert_function_expects(my_func_dots, c("a", "b"))) + # Succeeds when checking for a parameter with a default value + expect_true(assert_function_expects(my_func, c("c"))) + + # Function missing one of the required parameters my_func2 <- function(a, c) { a + c } + + # Errors when a required parameter is absent from the signature expect_error( assert_function_expects(my_func2, c("a", "b")), "Function 'my_func2' is missing the following parameter in its signature: `b`" ) - expect_error(assert_function_expects(123, "a"), "'123' must be a function, not a numeric") - expect_error(assert_function_expects(fn_with_no_required, "a"), - "Function 'fn_with_no_required' is missing the following parameter in its signature: `a`") + + # Errors when input is not a function + expect_error( + assert_function_expects(123, "a"), + "'123' must be a function, not a numeric" + ) + + # Errors when function has no matching required parameters + expect_error( + assert_function_expects(fn_with_no_required, "a"), + "Function 'fn_with_no_required' is missing the following parameter in its signature: `a`" + ) + + # Succeeds when required parameter exists even if it has a default expect_true(assert_function_expects(fn_with_defaults, "x")) - expect_error(assert_function_expects(my_func, c("a", "d"), msg = "Custom error message"), "Custom error message") + + # Uses custom error message when provided + expect_error( + assert_function_expects(my_func, c("a", "d"), msg = "Custom error message"), + "Custom error message" + ) + }) + From 398136cba9a7f0ce58007503babe0c48785848df Mon Sep 17 00:00:00 2001 From: selkamand <73202525+selkamand@users.noreply.github.com> Date: Mon, 9 Feb 2026 00:13:32 +1100 Subject: [PATCH 6/6] Add examples for assert_function_expects --- R/assert_functions.R | 8 ++++++++ man/assert_function_expects.Rd | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/R/assert_functions.R b/R/assert_functions.R index 7c96aa2..118b8c1 100644 --- a/R/assert_functions.R +++ b/R/assert_functions.R @@ -84,5 +84,13 @@ assert_function_expects_n_arguments <- assert_create(func = function_expects_n_a #' @return invisible(TRUE) if function `x` declares all required parameters, #' otherwise aborts with the error message specified by `msg` #' +#' @examples +#' my_fun <- function(x, y = 1, ...) x + y +#' assert_function_expects(my_fun, c("x", "y")) +#' +#' try({ +#' assert_function_expects(my_fun, c("x", "z")) +#' }) +#' #' @export assert_function_expects <- assert_create(func = function_expects_advanced) diff --git a/man/assert_function_expects.Rd b/man/assert_function_expects.Rd index b3a3802..bdb71bd 100644 --- a/man/assert_function_expects.Rd +++ b/man/assert_function_expects.Rd @@ -33,3 +33,11 @@ Assert that a function signature includes required set of parameter names in its formal argument list, regardless of whether those parameters have default values. The \code{...} argument is ignored. } +\examples{ +my_fun <- function(x, y = 1, ...) x + y +assert_function_expects(my_fun, c("x", "y")) + +try({ + assert_function_expects(my_fun, c("x", "z")) +}) +}