From f38d2e20b06716e994aed9dc241b216b572fea2c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 16:26:38 +0000 Subject: [PATCH 1/2] refactor: move optional arguments of motifs-graphlets functions behind the ellipsis Insert `...` between the defining arguments and the optional modifiers of 6 functions, following the zoning rules in CONTRIBUTING.md. Legacy positional and abbreviated calls are recovered by the generated ARG_HANDLE blocks (registry: tools/migrations/motifs-graphlets.R) and emit a single soft deprecation for igraph 3.0.0. No defaults change and no arguments are renamed. Functions: count_motifs, graphlet_basis, graphlet_proj, graphlets, motifs, sample_motifs Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016M32izVHZPfxAqemAe4BrX --- R/cycles.R | 1 + R/glet.R | 87 ++++++++++++++++++++++++++- R/motifs.R | 93 ++++++++++++++++++++++++++++- man/count_motifs.Rd | 4 +- man/graphlet_basis.Rd | 7 ++- man/motifs.Rd | 4 +- man/sample_motifs.Rd | 3 + tools/migrations/motifs-graphlets.R | 75 +++++++++++++++++++++++ 8 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 tools/migrations/motifs-graphlets.R 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/glet.R b/R/glet.R index 07a32999aef..81e97cda8ad 100644 --- a/R/glet.R +++ b/R/glet.R @@ -64,6 +64,7 @@ graphlets.candidate.basis <- function(graph, weights = NULL) { #' #' @param graph The input graph, edge directions are ignored. Only simple graph #' (i.e. graphs without self-loops and multiple edges) are supported. +#' @inheritParams rlang::args_dots_empty #' @param weights Edge weights. If the graph has a `weight` edge attribute #' and this argument is `NULL` (the default), then the `weight` edge #' attribute is used. @@ -135,7 +136,33 @@ graphlets.candidate.basis <- function(graph, weights = NULL) { #' } #' @family glet #' @export -graphlet_basis <- function(graph, weights = NULL) { +graphlet_basis <- function( + graph, + ..., + weights = NULL +) { + # BEGIN GENERATED ARG_HANDLE: graphlet_basis, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(weights = weights), + recover_new = c("weights"), + recover_old = c("weights"), + match_names = c("weights"), + match_to = c("weights"), + defaults = list(weights = NULL), + head_args = c("graph"), + fn_name = "graphlet_basis" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + graphlets_candidate_basis_impl( graph = graph, weights = weights @@ -143,14 +170,42 @@ graphlet_basis <- function(graph, weights = NULL) { } #' @rdname graphlet_basis +#' @inheritParams rlang::args_dots_empty #' @export graphlet_proj <- function( graph, + ..., weights = NULL, cliques, niter = 1000, Mu = rep(1, length(cliques)) ) { + # BEGIN GENERATED ARG_HANDLE: graphlet_proj, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(weights = weights, niter = niter, Mu = Mu), + recover_new = c("weights", "cliques", "niter", "Mu"), + recover_old = c("weights", "cliques", "niter", "Mu"), + match_names = c("weights", "cliques", "niter", "Mu"), + match_to = c("weights", "cliques", "niter", "Mu"), + defaults = list( + weights = NULL, + niter = 1000, + Mu = rep(1, length(cliques)) + ), + head_args = c("graph"), + fn_name = "graphlet_proj" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # Argument checks ensure_igraph(graph) if (is.null(weights) && "weight" %in% edge_attr_names(graph)) { @@ -228,8 +283,36 @@ function() { } #' @rdname graphlet_basis +#' @inheritParams rlang::args_dots_empty #' @export -graphlets <- function(graph, weights = NULL, niter = 1000) { +graphlets <- function( + graph, + ..., + weights = NULL, + niter = 1000 +) { + # BEGIN GENERATED ARG_HANDLE: graphlets, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(weights = weights, niter = niter), + recover_new = c("weights", "niter"), + recover_old = c("weights", "niter"), + match_names = c("weights", "niter"), + match_to = c("weights", "niter"), + defaults = list(weights = NULL, niter = 1000), + head_args = c("graph"), + fn_name = "graphlets" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + graphlets_impl( graph = graph, weights = weights, diff --git a/R/motifs.R b/R/motifs.R index c38806c1934..9449cdfabe4 100644 --- a/R/motifs.R +++ b/R/motifs.R @@ -132,6 +132,7 @@ dyad.census <- function(graph) { #' @param graph Graph object, the input graph. #' @param size The size of the motif, currently sizes 3 and 4 are supported in #' directed graphs and sizes 3 to 6 in undirected graphs. +#' @inheritParams rlang::args_dots_empty #' @param cut.prob Numeric vector giving the probabilities that the search #' graph is cut at a certain level. Its length should be the same as the size #' of the motif (the `size` argument). @@ -171,7 +172,35 @@ dyad.census <- function(graph) { #' count <<- count + 1 #' count < 5 # stop after 5 motifs #' }) -motifs <- function(graph, size = 3, cut.prob = NULL, callback = NULL) { +motifs <- function( + graph, + size = 3, + ..., + cut.prob = NULL, + callback = NULL +) { + # BEGIN GENERATED ARG_HANDLE: motifs, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(cut.prob = cut.prob, callback = callback), + recover_new = c("cut.prob", "callback"), + recover_old = c("cut.prob", "callback"), + match_names = c("cut.prob", "callback"), + match_to = c("cut.prob", "callback"), + defaults = list(cut.prob = NULL, callback = NULL), + head_args = c("graph", "size"), + fn_name = "motifs" + ) + 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(cut.prob) && length(cut.prob) != size) { cli::cli_abort("{.arg cut.prob} must be the same length as {.arg size}") } @@ -208,6 +237,7 @@ motifs <- function(graph, size = 3, cut.prob = NULL, callback = NULL) { #' #' @param graph Graph object, the input graph. #' @param size The size of the motif. +#' @inheritParams rlang::args_dots_empty #' @param cut.prob Numeric vector giving the probabilities that the search #' graph is cut at a certain level. Its length should be the same as the size #' of the motif (the `size` argument). @@ -223,7 +253,34 @@ motifs <- function(graph, size = 3, cut.prob = NULL, callback = NULL) { #' motifs(g, 3) #' count_motifs(g, 3) #' sample_motifs(g, 3) -count_motifs <- function(graph, size = 3, cut.prob = NULL) { +count_motifs <- function( + graph, + size = 3, + ..., + cut.prob = NULL +) { + # BEGIN GENERATED ARG_HANDLE: count_motifs, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(cut.prob = cut.prob), + recover_new = c("cut.prob"), + recover_old = c("cut.prob"), + match_names = c("cut.prob"), + match_to = c("cut.prob"), + defaults = list(cut.prob = NULL), + head_args = c("graph", "size"), + fn_name = "count_motifs" + ) + 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) if (!is.null(cut.prob) && length(cut.prob) != size) { @@ -248,6 +305,7 @@ count_motifs <- function(graph, size = 3, cut.prob = NULL) { #' @param graph Graph object, the input graph. #' @param size The size of the motif, currently size 3 and 4 are supported #' in directed graphs and sizes 3-6 in undirected graphs. +#' @inheritParams rlang::args_dots_empty #' @param cut.prob Numeric vector giving the probabilities that the search #' graph is cut at a certain level. Its length should be the same as the size #' of the motif (the `size` argument). @@ -272,10 +330,41 @@ count_motifs <- function(graph, size = 3, cut.prob = NULL) { sample_motifs <- function( graph, size = 3, + ..., cut.prob = rep(0, size), sample.size = NULL, sample = NULL ) { + # BEGIN GENERATED ARG_HANDLE: sample_motifs, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list( + cut.prob = cut.prob, + sample.size = sample.size, + sample = sample + ), + recover_new = c("cut.prob", "sample.size", "sample"), + recover_old = c("cut.prob", "sample.size", "sample"), + match_names = c("cut.prob", "sample.size", "sample"), + match_to = c("cut.prob", "sample.size", "sample"), + defaults = list( + cut.prob = rep(0, size), + sample.size = NULL, + sample = NULL + ), + head_args = c("graph", "size"), + fn_name = "sample_motifs" + ) + 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) if (!is.null(cut.prob) && length(cut.prob) != size) { diff --git a/man/count_motifs.Rd b/man/count_motifs.Rd index 4b03d0fe5ca..3c8e663d0cd 100644 --- a/man/count_motifs.Rd +++ b/man/count_motifs.Rd @@ -4,13 +4,15 @@ \alias{count_motifs} \title{Graph motifs} \usage{ -count_motifs(graph, size = 3, cut.prob = NULL) +count_motifs(graph, size = 3, ..., cut.prob = NULL) } \arguments{ \item{graph}{Graph object, the input graph.} \item{size}{The size of the motif.} +\item{...}{These dots are for future extensions and must be empty.} + \item{cut.prob}{Numeric vector giving the probabilities that the search graph is cut at a certain level. Its length should be the same as the size of the motif (the \code{size} argument). diff --git a/man/graphlet_basis.Rd b/man/graphlet_basis.Rd index 045b239d4b0..1fd9f5783d4 100644 --- a/man/graphlet_basis.Rd +++ b/man/graphlet_basis.Rd @@ -6,22 +6,25 @@ \alias{graphlets} \title{Graphlet decomposition of a graph} \usage{ -graphlet_basis(graph, weights = NULL) +graphlet_basis(graph, ..., weights = NULL) graphlet_proj( graph, + ..., weights = NULL, cliques, niter = 1000, Mu = rep(1, length(cliques)) ) -graphlets(graph, weights = NULL, niter = 1000) +graphlets(graph, ..., weights = NULL, niter = 1000) } \arguments{ \item{graph}{The input graph, edge directions are ignored. Only simple graph (i.e. graphs without self-loops and multiple edges) are supported.} +\item{...}{These dots are for future extensions and must be empty.} + \item{weights}{Edge weights. If the graph has a \code{weight} edge attribute and this argument is \code{NULL} (the default), then the \code{weight} edge attribute is used.} diff --git a/man/motifs.Rd b/man/motifs.Rd index 66ed6292cbe..28a9bdbc124 100644 --- a/man/motifs.Rd +++ b/man/motifs.Rd @@ -4,7 +4,7 @@ \alias{motifs} \title{Graph motifs} \usage{ -motifs(graph, size = 3, cut.prob = NULL, callback = NULL) +motifs(graph, size = 3, ..., cut.prob = NULL, callback = NULL) } \arguments{ \item{graph}{Graph object, the input graph.} @@ -12,6 +12,8 @@ motifs(graph, size = 3, cut.prob = NULL, callback = NULL) \item{size}{The size of the motif, currently sizes 3 and 4 are supported in directed graphs and sizes 3 to 6 in undirected graphs.} +\item{...}{These dots are for future extensions and must be empty.} + \item{cut.prob}{Numeric vector giving the probabilities that the search graph is cut at a certain level. Its length should be the same as the size of the motif (the \code{size} argument). diff --git a/man/sample_motifs.Rd b/man/sample_motifs.Rd index c2c86981c5d..266ca0665f5 100644 --- a/man/sample_motifs.Rd +++ b/man/sample_motifs.Rd @@ -7,6 +7,7 @@ sample_motifs( graph, size = 3, + ..., cut.prob = rep(0, size), sample.size = NULL, sample = NULL @@ -18,6 +19,8 @@ sample_motifs( \item{size}{The size of the motif, currently size 3 and 4 are supported in directed graphs and sizes 3-6 in undirected graphs.} +\item{...}{These dots are for future extensions and must be empty.} + \item{cut.prob}{Numeric vector giving the probabilities that the search graph is cut at a certain level. Its length should be the same as the size of the motif (the \code{size} argument). diff --git a/tools/migrations/motifs-graphlets.R b/tools/migrations/motifs-graphlets.R new file mode 100644 index 00000000000..21f26a2012e --- /dev/null +++ b/tools/migrations/motifs-graphlets.R @@ -0,0 +1,75 @@ +# Argument-signature migrations: motifs-graphlets +# Schema: see tools/migrations/README.md. Regenerate with: +# Rscript tools/generate-migrations.R + +migrations <- list( + graphlet_basis = list( + old = function(graph, weights) {}, + new = function( + graph, + ..., + weights = NULL + ) {}, + when = "3.0.0" + ), + + graphlet_proj = list( + old = function(graph, weights, cliques, niter, Mu) {}, + new = function( + graph, + ..., + weights = NULL, + cliques, + niter = 1000, + Mu = rep(1, length(cliques)) + ) {}, + when = "3.0.0" + ), + + graphlets = list( + old = function(graph, weights, niter) {}, + new = function( + graph, + ..., + weights = NULL, + niter = 1000 + ) {}, + when = "3.0.0" + ), + + count_motifs = list( + old = function(graph, size, cut.prob) {}, + new = function( + graph, + size = 3, + ..., + cut.prob = NULL + ) {}, + when = "3.0.0" + ), + + motifs = list( + old = function(graph, size, cut.prob, callback) {}, + new = function( + graph, + size = 3, + ..., + cut.prob = NULL, + callback = NULL + ) {}, + when = "3.0.0" + ), + + sample_motifs = list( + old = function(graph, size, cut.prob, sample.size, sample) {}, + new = function( + graph, + size = 3, + ..., + cut.prob = rep(0, size), + sample.size = NULL, + sample = NULL + ) {}, + when = "3.0.0" + ) +) From afe604ed3c4ce5878e0d60950f8396cf8e6fc37c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 20:20:24 +0000 Subject: [PATCH 2/2] test: cover migrated motifs-graphlets signatures Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016M32izVHZPfxAqemAe4BrX --- tests/testthat/test-glet.R | 80 ++++++++++++++++++++++++++++++++++++ tests/testthat/test-motifs.R | 53 ++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/tests/testthat/test-glet.R b/tests/testthat/test-glet.R index c15ae44cf92..af039389da0 100644 --- a/tests/testthat/test-glet.R +++ b/tests/testthat/test-glet.R @@ -79,3 +79,83 @@ test_that("Graphlet projection works", { expect_equal(glp, glp2) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +# Shared fixture: the weighted overlapping-groups graph from the examples. +# Its `weight` edge attribute has four distinct threshold levels. +make_graphlet_graph <- function() { + D1 <- matrix(0, 5, 5) + D2 <- matrix(0, 5, 5) + D3 <- matrix(0, 5, 5) + D1[1:3, 1:3] <- 2 + D2[3:5, 3:5] <- 3 + D3[2:5, 2:5] <- 1 + simplify(graph_from_adjacency_matrix( + D1 + D2 + D3, + mode = "undirected", + weighted = TRUE + )) +} + +test_that("graphlet_basis() covers weights", { + g <- make_graphlet_graph() + + # The attribute weights yield a four-clique candidate basis, + # uniform explicit weights override them and collapse the basis + # to the two maximal cliques. + expect_length(graphlet_basis(g)$cliques, 4) + res <- graphlet_basis(g, weights = rep(1, ecount(g))) + expect_named(res, c("cliques", "thresholds")) + expect_length(res$cliques, 2) + expect_equal(res$thresholds, c(1, 1)) + + # Legacy positional `weights` is recovered with a deprecation warning. + lifecycle::expect_deprecated( + res_legacy <- graphlet_basis(g, rep(1, ecount(g))) + ) + expect_identical(res_legacy, res) +}) + +test_that("graphlet_proj() covers all tail arguments", { + g <- make_graphlet_graph() + cl <- graphlet_basis(g)$cliques + + # The projection returns one non-negative weight per basis clique. + res <- graphlet_proj( + g, + weights = E(g)$weight, + cliques = cl, + niter = 100, + Mu = rep(2, length(cl)) + ) + expect_type(res, "double") + expect_length(res, length(cl)) + expect_true(all(res >= 0)) + + # Legacy positional `weights` is recovered with a deprecation warning. + lifecycle::expect_deprecated( + res_legacy <- graphlet_proj(g, E(g)$weight, cliques = cl) + ) + expect_identical( + res_legacy, + graphlet_proj(g, weights = E(g)$weight, cliques = cl) + ) +}) + +test_that("graphlets() covers all tail arguments", { + g <- make_graphlet_graph() + w <- rep(1, ecount(g)) + + # Uniform weights collapse the basis to the two maximal cliques. + res <- graphlets(g, weights = w, niter = 50) + expect_named(res, c("cliques", "Mu")) + expect_length(res$cliques, 2) + expect_length(res$Mu, length(res$cliques)) + + # Legacy positional `weights` is recovered with a deprecation warning. + lifecycle::expect_deprecated( + res_legacy <- graphlets(g, w) + ) + expect_identical(res_legacy, graphlets(g, weights = w)) +}) diff --git a/tests/testthat/test-motifs.R b/tests/testthat/test-motifs.R index 509885a7e25..1254bcdb30a 100644 --- a/tests/testthat/test-motifs.R +++ b/tests/testthat/test-motifs.R @@ -269,3 +269,56 @@ test_that("motifs with callback output matches expected", { motif_data[1:2] }) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("motifs() recovers legacy positional arguments", { + # `cut.prob` and `callback` are exercised by name above. + g <- make_graph(~ A - B - C - A - D - E - F - D - C - F) + + # An all-zero cut probability vector matches the default of no cuts. + lifecycle::expect_deprecated( + res <- motifs(g, 3, rep(0, 3)) + ) + expect_identical(res, motifs(g, 3, cut.prob = rep(0, 3))) + expect_identical(res, motifs(g, 3)) +}) + +test_that("count_motifs() recovers legacy positional arguments", { + # `cut.prob` is exercised by name above. + g <- make_graph(~ A - B - C - A - D - E - F - D - C - F) + + # Cutting the search with probability one at every level + # deterministically drops all motifs. + lifecycle::expect_deprecated( + res <- count_motifs(g, 3, c(1, 1, 1)) + ) + expect_identical(res, count_motifs(g, 3, cut.prob = c(1, 1, 1))) + expect_equal(res, 0) + expect_equal(count_motifs(g, size = 3), 12) +}) + +test_that("sample_motifs() covers cut.prob and sample.size", { + # `sample` is exercised by name above. + g <- make_graph(~ A - B - C - A - D - E - F - D - C - F) + + # Starting the search from every vertex makes the estimate exact. + igraph_with_seed(42, { + res <- sample_motifs( + g, + size = 3, + cut.prob = rep(0, 3), + sample.size = vcount(g) + ) + }) + expect_equal(res, count_motifs(g, size = 3)) + + # Legacy positional `cut.prob` and `sample.size` are recovered + # with a deprecation warning. + igraph_with_seed(42, { + lifecycle::expect_deprecated( + res_legacy <- sample_motifs(g, 3, rep(0, 3), vcount(g)) + ) + }) + expect_identical(res_legacy, res) +})