diff --git a/R/coloring.R b/R/coloring.R index f68804f728c..c72315caee6 100644 --- a/R/coloring.R +++ b/R/coloring.R @@ -12,6 +12,7 @@ #' linear time. #' #' @param graph The graph object to color. +#' @inheritParams rlang::args_dots_empty #' @param heuristic The selection heuristic for the next vertex to consider. #' Possible values are: \dQuote{colored_neighbors} selects the vertex with the #' largest number of already colored neighbors. \dQuote{dsatur} selects the @@ -32,8 +33,31 @@ #' greedy_vertex_coloring <- function( graph, + ..., heuristic = c("colored_neighbors", "dsatur") ) { + # BEGIN GENERATED ARG_HANDLE: greedy_vertex_coloring, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(heuristic = heuristic), + recover_new = c("heuristic"), + recover_old = c("heuristic"), + match_names = c("heuristic"), + match_to = c("heuristic"), + defaults = list(heuristic = c("colored_neighbors", "dsatur")), + head_args = c("graph"), + fn_name = "greedy_vertex_coloring" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + vertex_coloring_greedy_impl( graph = graph, heuristic = heuristic diff --git a/R/cycles.R b/R/cycles.R index c3d65b5cae0..25356203038 100644 --- a/R/cycles.R +++ b/R/cycles.R @@ -31,6 +31,7 @@ #' a specific cycle. #' #' @param graph The input graph. +#' @inheritParams rlang::args_dots_empty #' @param mode Character constant specifying how to handle directed graphs. #' `out` follows edge directions, `in` follows edges in the reverse direction, #' and `all` ignores edge directions. Ignored in undirected graphs. diff --git a/R/epi.R b/R/epi.R index c35367881b0..7e90b1e8ccc 100644 --- a/R/epi.R +++ b/R/epi.R @@ -20,8 +20,13 @@ ################################################################### #' @rdname sir +#' @inheritParams rlang::args_dots_empty #' @export -time_bins <- function(x, middle = TRUE) { +time_bins <- function( + x, + ..., + middle = TRUE +) { UseMethod("time_bins") } @@ -29,7 +34,29 @@ time_bins <- function(x, middle = TRUE) { #' @rdname sir #' @export #' @importFrom stats IQR -time_bins.sir <- function(x, middle = TRUE) { +time_bins.sir <- function(x, ..., middle = TRUE) { + # BEGIN GENERATED ARG_HANDLE: time_bins, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(middle = middle), + recover_new = c("middle"), + recover_old = c("middle"), + match_names = c("middle"), + match_to = c("middle"), + defaults = list(middle = TRUE), + head_args = c("x"), + fn_name = "time_bins" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + sir <- x big.time <- unlist(sapply(sir, "[[", "times")) diff --git a/R/foreign.R b/R/foreign.R index 402b67cf70a..0510ab201c7 100644 --- a/R/foreign.R +++ b/R/foreign.R @@ -794,6 +794,7 @@ write.graph.dot <- function(graph, file) { #' for the actual format of a graph database file and other information. #' #' @param url Complete URL with the file to import. Default: `NULL`. +#' @inheritParams rlang::args_dots_empty #' @param prefix Gives the prefix. See details below. Possible values: #' `iso`, `i2`, `si4`, `si6`, `mcs10`, `mcs30`, #' `mcs50`, `mcs70`, `mcs90`. @@ -825,6 +826,7 @@ write.graph.dot <- function(graph, file) { #' @keywords graphs graph_from_graphdb <- function( url = NULL, + ..., prefix = "iso", type = "r001", nodes = NULL, @@ -834,6 +836,82 @@ graph_from_graphdb <- function( compressed = TRUE, directed = TRUE ) { + # BEGIN GENERATED ARG_HANDLE: graph_from_graphdb, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list( + prefix = prefix, + type = type, + nodes = nodes, + pair = pair, + which = which, + base = base, + compressed = compressed, + directed = directed + ), + recover_new = c( + "prefix", + "type", + "nodes", + "pair", + "which", + "base", + "compressed", + "directed" + ), + recover_old = c( + "prefix", + "type", + "nodes", + "pair", + "which", + "base", + "compressed", + "directed" + ), + match_names = c( + "prefix", + "type", + "nodes", + "pair", + "which", + "base", + "compressed", + "directed" + ), + match_to = c( + "prefix", + "type", + "nodes", + "pair", + "which", + "base", + "compressed", + "directed" + ), + defaults = list( + prefix = "iso", + type = "r001", + nodes = NULL, + pair = "A", + which = 0, + base = "https://github.com/igraph/graphsdb/raw/refs/heads/main", + compressed = TRUE, + directed = TRUE + ), + head_args = c("url"), + fn_name = "graph_from_graphdb" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + if (is.null(nodes) && is.null(url)) { cli::cli_abort("Either {.arg nodes}' or `{.arg url}' must be non-null.") } diff --git a/R/sir.R b/R/sir.R index 629a3a203b3..34d3c8d9497 100644 --- a/R/sir.R +++ b/R/sir.R @@ -53,6 +53,7 @@ #' @param gamma Positive scalar. The rate of recovery of an infected #' individual. Formally, this is the rate parameter of an exponential #' distribution. +#' @inheritParams rlang::args_dots_empty #' @param no.sim Integer scalar, the number simulation runs to perform. #' @param x A `sir` object, returned by the `sir()` function. #' @param middle Logical, whether to return the middle of the time bins, @@ -108,7 +109,35 @@ #' plot(sm) #' @family processes #' @export -sir <- function(graph, beta, gamma, no.sim = 100) { +sir <- function( + graph, + beta, + gamma, + ..., + no.sim = 100 +) { + # BEGIN GENERATED ARG_HANDLE: sir, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(no.sim = no.sim), + recover_new = c("no.sim"), + recover_old = c("no.sim"), + match_names = c("no.sim"), + match_to = c("no.sim"), + defaults = list(no.sim = 100), + head_args = c("graph", "beta", "gamma"), + fn_name = "sir" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + sir_impl( graph = graph, beta = beta, diff --git a/R/stochastic_matrix.R b/R/stochastic_matrix.R index 800daffa7be..3d22332ebe6 100644 --- a/R/stochastic_matrix.R +++ b/R/stochastic_matrix.R @@ -51,6 +51,7 @@ get.stochastic <- function( #' matrices are defined in a symmetric way. #' #' @param graph The input graph. Must be of class `igraph`. +#' @inheritParams rlang::args_dots_empty #' @param column.wise If `FALSE`, then the rows of the stochastic matrix #' sum up to one; otherwise it is the columns. #' @param sparse Logical, whether to return a sparse matrix. The @@ -76,9 +77,35 @@ get.stochastic <- function( #' stochastic_matrix <- function( graph, + ..., column.wise = FALSE, sparse = igraph_opt("sparsematrices") ) { + # BEGIN GENERATED ARG_HANDLE: stochastic_matrix, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(column.wise = column.wise, sparse = sparse), + recover_new = c("column.wise", "sparse"), + recover_old = c("column.wise", "sparse"), + match_names = c("column.wise", "sparse"), + match_to = c("column.wise", "sparse"), + defaults = list( + column.wise = FALSE, + sparse = igraph_opt("sparsematrices") + ), + head_args = c("graph"), + fn_name = "stochastic_matrix" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + ensure_igraph(graph) column.wise <- as.logical(column.wise) diff --git a/man/graph_from_graphdb.Rd b/man/graph_from_graphdb.Rd index 98c61850f81..3b87c241a5c 100644 --- a/man/graph_from_graphdb.Rd +++ b/man/graph_from_graphdb.Rd @@ -6,6 +6,7 @@ \usage{ graph_from_graphdb( url = NULL, + ..., prefix = "iso", type = "r001", nodes = NULL, @@ -19,6 +20,8 @@ graph_from_graphdb( \arguments{ \item{url}{Complete URL with the file to import. Default: \code{NULL}.} +\item{...}{These dots are for future extensions and must be empty.} + \item{prefix}{Gives the prefix. See details below. Possible values: \code{iso}, \code{i2}, \code{si4}, \code{si6}, \code{mcs10}, \code{mcs30}, \code{mcs50}, \code{mcs70}, \code{mcs90}.} diff --git a/man/greedy_vertex_coloring.Rd b/man/greedy_vertex_coloring.Rd index 767ec078d27..cf0a5e4b8b9 100644 --- a/man/greedy_vertex_coloring.Rd +++ b/man/greedy_vertex_coloring.Rd @@ -4,11 +4,17 @@ \alias{greedy_vertex_coloring} \title{Greedy vertex coloring} \usage{ -greedy_vertex_coloring(graph, heuristic = c("colored_neighbors", "dsatur")) +greedy_vertex_coloring( + graph, + ..., + heuristic = c("colored_neighbors", "dsatur") +) } \arguments{ \item{graph}{The graph object to color.} +\item{...}{These dots are for future extensions and must be empty.} + \item{heuristic}{The selection heuristic for the next vertex to consider. Possible values are: \dQuote{colored_neighbors} selects the vertex with the largest number of already colored neighbors. \dQuote{dsatur} selects the diff --git a/man/sir.Rd b/man/sir.Rd index d7afbfc5394..50cb9cc3f58 100644 --- a/man/sir.Rd +++ b/man/sir.Rd @@ -8,19 +8,21 @@ \alias{sir} \title{SIR model on graphs} \usage{ -time_bins(x, middle = TRUE) +time_bins(x, ..., middle = TRUE) -\method{time_bins}{sir}(x, middle = TRUE) +\method{time_bins}{sir}(x, ..., middle = TRUE) \method{median}{sir}(x, na.rm = FALSE, ...) \method{quantile}{sir}(x, comp = c("NI", "NS", "NR"), prob, ...) -sir(graph, beta, gamma, no.sim = 100) +sir(graph, beta, gamma, ..., no.sim = 100) } \arguments{ \item{x}{A \code{sir} object, returned by the \code{sir()} function.} +\item{\dots}{Additional arguments, ignored currently.} + \item{middle}{Logical, whether to return the middle of the time bins, or the boundaries.} @@ -28,8 +30,6 @@ or the boundaries.} objects do not contain any \code{NA} values currently, so this argument is effectively ignored.} -\item{\dots}{Additional arguments, ignored currently.} - \item{comp}{Character scalar. The component to calculate the quantile of. \code{NI} is infected agents, \code{NS} is susceptibles, \code{NR} stands for recovered.} diff --git a/man/stochastic_matrix.Rd b/man/stochastic_matrix.Rd index 74f19821678..df24220d9d0 100644 --- a/man/stochastic_matrix.Rd +++ b/man/stochastic_matrix.Rd @@ -6,6 +6,7 @@ \usage{ stochastic_matrix( graph, + ..., column.wise = FALSE, sparse = igraph_opt("sparsematrices") ) @@ -13,6 +14,8 @@ stochastic_matrix( \arguments{ \item{graph}{The input graph. Must be of class \code{igraph}.} +\item{...}{These dots are for future extensions and must be empty.} + \item{column.wise}{If \code{FALSE}, then the rows of the stochastic matrix sum up to one; otherwise it is the columns.} diff --git a/tests/testthat/test-coloring.R b/tests/testthat/test-coloring.R index 45e4edd5536..ffbe5ac81bb 100644 --- a/tests/testthat/test-coloring.R +++ b/tests/testthat/test-coloring.R @@ -32,3 +32,15 @@ test_that("simplify_and_colorize works", { expect_equal(V(result)$color, c(0, 0, 0, 0, 1)) expect_equal(E(result)$color, c(1, 4, 1, 2)) }) + +# ---- ellipsis migration: argument coverage ---------------------------------- +# The `heuristic` tail argument is already exercised by name in the tests above, +# so here we only cover the legacy positional recovery path. + +test_that("greedy_vertex_coloring() recovers a legacy positional `heuristic`", { + g <- make_star(10, mode = "undirected") + lifecycle::expect_deprecated( + col <- greedy_vertex_coloring(g, "dsatur") + ) + expect_identical(col, greedy_vertex_coloring(g, heuristic = "dsatur")) +}) diff --git a/tests/testthat/test-epi.R b/tests/testthat/test-epi.R new file mode 100644 index 00000000000..8935e0f4df7 --- /dev/null +++ b/tests/testthat/test-epi.R @@ -0,0 +1,25 @@ +# ---- ellipsis migration: argument coverage ---------------------------------- +# `time_bins()` lives in R/epi.R and dispatches through the `sir` method, so the +# migrated signature is exercised via `time_bins.sir()`. + +test_that("time_bins() covers the `middle` tail argument", { + igraph_local_seed(42) + res <- sir(sample_gnm(50, 50), beta = 5, gamma = 1, no.sim = 20) + middles <- time_bins(res, middle = TRUE) + boundaries <- time_bins(res, middle = FALSE) + # Middles are the midpoints between consecutive boundaries: one fewer value, + # each lying strictly inside its bin. + expect_length(middles, length(boundaries) - 1L) + expect_true(all( + middles > boundaries[-length(boundaries)] & middles < boundaries[-1] + )) +}) + +test_that("time_bins() recovers a legacy positional `middle`", { + igraph_local_seed(42) + res <- sir(sample_gnm(50, 50), beta = 5, gamma = 1, no.sim = 20) + lifecycle::expect_deprecated( + boundaries <- time_bins(res, FALSE) + ) + expect_identical(boundaries, time_bins(res, middle = FALSE)) +}) diff --git a/tests/testthat/test-foreign.R b/tests/testthat/test-foreign.R index d15d9de395c..d0c29e32e7b 100644 --- a/tests/testthat/test-foreign.R +++ b/tests/testthat/test-foreign.R @@ -75,3 +75,53 @@ test_that("graph_from_graphdb works", { graph_from_graphdb(nodes = 10, type = "not_existing") ) }) + +# ---- ellipsis migration: argument coverage ---------------------------------- + +test_that("graph_from_graphdb() accepts every tail argument by name", { + # A real download is required to build a graph, so this runs on the CI hosts + # that can reach the graph database. It mirrors the guards of the + # `graph_from_graphdb works` test and additionally skips (rather than fails) + # when the database is unreachable, e.g. sandboxed mirrors returning 403. + skip_on_os("windows") + skip_on_cran() + skip_if(Sys.getenv("R_SANITIZER") == "true") + + # suppressWarnings(): an unreachable database makes `file()` emit a "cannot + # open URL" warning before the connection error we catch below; muffle it so + # the skip is clean. A successful download is silent, so nothing is hidden. + g <- tryCatch( + suppressWarnings(graph_from_graphdb( + url = NULL, + prefix = "iso", + type = "r001", + nodes = 1000, + pair = "A", + which = 0, + base = "https://github.com/igraph/graphsdb/raw/refs/heads/main", + compressed = TRUE, + directed = FALSE + )), + error = function(e) e + ) + if (inherits(g, "condition")) { + skip(paste("graph database unreachable:", conditionMessage(g))) + } + expect_true(is_igraph(g)) + expect_false(is_directed(g)) + expect_vcount(g, 1000) +}) + +test_that("graph_from_graphdb() wires up legacy positional recovery", { + # A successful recovery would need the network, so we exercise the path with an + # offline validation error instead: a legacy positional `prefix` routes to the + # `prefix` argument, and an invalid value raises the prefix-specific error + # *after* the deprecation warning -- proving the value reached `prefix` without + # a download. + lifecycle::expect_deprecated( + expect_error( + graph_from_graphdb(NULL, "not_existing", nodes = 10), + regexp = "not a valid prefix" + ) + ) +}) diff --git a/tests/testthat/test-sir.R b/tests/testthat/test-sir.R index b3d4956a3d3..c80048b036f 100644 --- a/tests/testthat/test-sir.R +++ b/tests/testthat/test-sir.R @@ -95,3 +95,31 @@ test_that("SIR works", { expect_true(all(diff(res[[10]]$NR) >= 0)) expect_true(all(res[[10]]$NS + res[[10]]$NI + res[[10]]$NR == 50)) }) + +# ---- ellipsis migration: argument coverage ---------------------------------- + +test_that("sir() covers `no.sim` and the `beta` infection rate", { + igraph_local_seed(1) + g <- sample_gnm(60, 120) + # `no.sim` is the sole tail argument and fixes the number of runs. + # `beta = 0` means no susceptible is ever infected, so every run ends with + # just the single seed individual recovered and everyone else susceptible. + res <- sir(g, beta = 0, gamma = 1, no.sim = 15) + expect_s3_class(res, "sir") + expect_length(res, 15) + final_nr <- vapply(res, function(s) s$NR[length(s$NR)], double(1)) + final_ns <- vapply(res, function(s) s$NS[length(s$NS)], double(1)) + expect_true(all(final_nr == 1)) + expect_true(all(final_ns == vcount(g) - 1)) +}) + +test_that("sir() recovers a legacy positional `no.sim`", { + g <- sample_gnm(50, 50) + igraph_local_seed(20231029) + lifecycle::expect_deprecated( + res_positional <- sir(g, 5, 1, 20) + ) + igraph_local_seed(20231029) + res_named <- sir(g, 5, 1, no.sim = 20) + expect_identical(res_positional, res_named) +}) diff --git a/tests/testthat/test-stochastic_matrix.R b/tests/testthat/test-stochastic_matrix.R index 036efb5f32b..92d0bd7a273 100644 --- a/tests/testthat/test-stochastic_matrix.R +++ b/tests/testthat/test-stochastic_matrix.R @@ -14,3 +14,26 @@ test_that("stochastic_matrix works", { as_unnamed_dense_matrix(stoch_mat_calc_col) ) }) + +# ---- ellipsis migration: argument coverage ---------------------------------- +# `column.wise` is covered by name above; here we add the `sparse` tail argument +# and the legacy positional recovery path. + +test_that("stochastic_matrix() returns a sparse matrix when `sparse = TRUE`", { + g <- make_star(5, "undirected") + W <- stochastic_matrix(g, sparse = TRUE) + expect_s4_class(W, "Matrix") + # The sparse and dense forms describe the same stochastic matrix. + expect_equal( + as_unnamed_dense_matrix(W), + as_unnamed_dense_matrix(stochastic_matrix(g, sparse = FALSE)) + ) +}) + +test_that("stochastic_matrix() recovers a legacy positional `column.wise`", { + g <- make_star(5, "undirected") + lifecycle::expect_deprecated( + W <- stochastic_matrix(g, TRUE) + ) + expect_identical(W, stochastic_matrix(g, column.wise = TRUE)) +}) diff --git a/tools/migrations/misc.R b/tools/migrations/misc.R new file mode 100644 index 00000000000..32bd22ca254 --- /dev/null +++ b/tools/migrations/misc.R @@ -0,0 +1,75 @@ +# Argument-signature migrations: misc +# Schema: see tools/migrations/README.md. Regenerate with: +# Rscript tools/generate-migrations.R + +migrations <- list( + greedy_vertex_coloring = list( + old = function(graph, heuristic) {}, + new = function( + graph, + ..., + heuristic = c("colored_neighbors", "dsatur") + ) {}, + when = "3.0.0" + ), + + time_bins = list( + old = function(x, middle) {}, + new = function( + x, + ..., + middle = TRUE + ) {}, + when = "3.0.0" + ), + + graph_from_graphdb = list( + old = function( + url, + prefix, + type, + nodes, + pair, + which, + base, + compressed, + directed + ) {}, + new = function( + url = NULL, + ..., + prefix = "iso", + type = "r001", + nodes = NULL, + pair = "A", + which = 0, + base = "https://github.com/igraph/graphsdb/raw/refs/heads/main", + compressed = TRUE, + directed = TRUE + ) {}, + when = "3.0.0" + ), + + sir = list( + old = function(graph, beta, gamma, no.sim) {}, + new = function( + graph, + beta, + gamma, + ..., + no.sim = 100 + ) {}, + when = "3.0.0" + ), + + stochastic_matrix = list( + old = function(graph, column.wise, sparse) {}, + new = function( + graph, + ..., + column.wise = FALSE, + sparse = igraph_opt("sparsematrices") + ) {}, + when = "3.0.0" + ) +)