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/par.R b/R/par.R index 08d865d99fb..36bfd03fb14 100644 --- a/R/par.R +++ b/R/par.R @@ -295,8 +295,35 @@ get_all_options <- function() { } #' @rdname igraph_options +#' @inheritParams rlang::args_dots_empty #' @export -igraph_opt <- function(x, default = NULL) { +igraph_opt <- function( + x, + ..., + default = NULL +) { + # BEGIN GENERATED ARG_HANDLE: igraph_opt, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(default = default), + recover_new = c("default"), + recover_old = c("default"), + match_names = c("default"), + match_to = c("default"), + defaults = list(default = NULL), + head_args = c("x"), + fn_name = "igraph_opt" + ) + 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 (missing(default)) { get_config(paste0("igraph::", x), .igraph.pars[[x]]) } else { diff --git a/R/plot.common.R b/R/plot.common.R index 70c903f534d..e578de48011 100644 --- a/R/plot.common.R +++ b/R/plot.common.R @@ -707,6 +707,7 @@ i.postprocess.layout <- function(maybe_layout) { #' plotting a graph with multiple edges, so that all edges are visible. #' #' @param graph The input graph. +#' @inheritParams rlang::args_dots_empty #' @param start The curvature at the two extreme edges. All edges will have a #' curvature between `-start` and `start`, spaced equally. #' @return A numeric vector, its length is the number of edges in the graph. @@ -730,7 +731,33 @@ i.postprocess.layout <- function(maybe_layout) { #' set.seed(42) #' plot(g) #' -curve_multiple <- function(graph, start = 0.5) { +curve_multiple <- function( + graph, + ..., + start = 0.5 +) { + # BEGIN GENERATED ARG_HANDLE: curve_multiple, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(start = start), + recover_new = c("start"), + recover_old = c("start"), + match_names = c("start"), + match_to = c("start"), + defaults = list(start = 0.5), + head_args = c("graph"), + fn_name = "curve_multiple" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + el <- apply(as_edgelist(graph, names = FALSE), 1, paste, collapse = ":") ave(rep(NA, length(el)), el, FUN = function(x) { if (length(x) == 1) { diff --git a/R/plot.shapes.R b/R/plot.shapes.R index 79a1196f3ba..e9fd0a4ad10 100644 --- a/R/plot.shapes.R +++ b/R/plot.shapes.R @@ -369,13 +369,41 @@ shape_noplot <- function(coords, v = NULL, params) { } #' @rdname shapes +#' @inheritParams rlang::args_dots_empty #' @export add_shape <- function( shape, + ..., clip = shape_noclip, plot = shape_noplot, parameters = list() ) { + # BEGIN GENERATED ARG_HANDLE: add_shape, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(clip = clip, plot = plot, parameters = parameters), + recover_new = c("clip", "plot", "parameters"), + recover_old = c("clip", "plot", "parameters"), + match_names = c("clip", "plot", "parameters"), + match_to = c("clip", "plot", "parameters"), + defaults = list( + clip = shape_noclip, + plot = shape_noplot, + parameters = list() + ), + head_args = c("shape"), + fn_name = "add_shape" + ) + 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.character(shape) || length(shape) != 1) { cli::cli_abort(c( "{.arg shape} must be a character of length 1.", diff --git a/R/tkplot.R b/R/tkplot.R index 15533d9d60a..6bffb3c2606 100644 --- a/R/tkplot.R +++ b/R/tkplot.R @@ -527,8 +527,35 @@ tkplot <- function(graph, canvas.width = 450, canvas.height = 450, ...) { ################################################################### #' @rdname tkplot +#' @inheritParams rlang::args_dots_empty #' @export -tk_close <- function(tkp.id, window.close = TRUE) { +tk_close <- function( + tkp.id, + ..., + window.close = TRUE +) { + # BEGIN GENERATED ARG_HANDLE: tk_close, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(window.close = window.close), + recover_new = c("window.close"), + recover_old = c("window.close"), + match_names = c("window.close"), + match_to = c("window.close"), + defaults = list(window.close = TRUE), + head_args = c("tkp.id"), + fn_name = "tk_close" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # nocov start if (window.close) { cmd <- paste(sep = "", "tkp.", tkp.id, "$top") @@ -555,8 +582,36 @@ tk_off <- function() { } #' @rdname tkplot +#' @inheritParams rlang::args_dots_empty #' @export -tk_fit <- function(tkp.id, width = NULL, height = NULL) { +tk_fit <- function( + tkp.id, + ..., + width = NULL, + height = NULL +) { + # BEGIN GENERATED ARG_HANDLE: tk_fit, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(width = width, height = height), + recover_new = c("width", "height"), + recover_old = c("width", "height"), + match_names = c("width", "height"), + match_to = c("width", "height"), + defaults = list(width = NULL, height = NULL), + head_args = c("tkp.id"), + fn_name = "tk_fit" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # nocov start tkp <- .tkplot.get(tkp.id) if (is.null(width)) { @@ -646,8 +701,35 @@ tk_postscript <- function(tkp.id) { } #' @rdname tkplot +#' @inheritParams rlang::args_dots_empty #' @export -tk_coords <- function(tkp.id, norm = FALSE) { +tk_coords <- function( + tkp.id, + ..., + norm = FALSE +) { + # BEGIN GENERATED ARG_HANDLE: tk_coords, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(norm = norm), + recover_new = c("norm"), + recover_old = c("norm"), + match_names = c("norm"), + match_to = c("norm"), + defaults = list(norm = FALSE), + head_args = c("tkp.id"), + fn_name = "tk_coords" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # nocov start coords <- .tkplot.get(tkp.id, "coords") coords[, 2] <- max(coords[, 2]) - coords[, 2] @@ -675,8 +757,36 @@ tk_set_coords <- function(tkp.id, coords) { } #' @rdname tkplot +#' @inheritParams rlang::args_dots_empty #' @export -tk_rotate <- function(tkp.id, degree = NULL, rad = NULL) { +tk_rotate <- function( + tkp.id, + ..., + degree = NULL, + rad = NULL +) { + # BEGIN GENERATED ARG_HANDLE: tk_rotate, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(degree = degree, rad = rad), + recover_new = c("degree", "rad"), + recover_old = c("degree", "rad"), + match_names = c("degree", "rad"), + match_to = c("degree", "rad"), + defaults = list(degree = NULL, rad = NULL), + head_args = c("tkp.id"), + fn_name = "tk_rotate" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # nocov start coords <- .tkplot.get(tkp.id, "coords") diff --git a/man/curve_multiple.Rd b/man/curve_multiple.Rd index 851137a1d34..fb9831ed2f9 100644 --- a/man/curve_multiple.Rd +++ b/man/curve_multiple.Rd @@ -4,11 +4,13 @@ \alias{curve_multiple} \title{Optimal edge curvature when plotting graphs} \usage{ -curve_multiple(graph, start = 0.5) +curve_multiple(graph, ..., start = 0.5) } \arguments{ \item{graph}{The input graph.} +\item{...}{These dots are for future extensions and must be empty.} + \item{start}{The curvature at the two extreme edges. All edges will have a curvature between \code{-start} and \code{start}, spaced equally.} } diff --git a/man/igraph_options.Rd b/man/igraph_options.Rd index 297446ce7d6..8ba971f89fc 100644 --- a/man/igraph_options.Rd +++ b/man/igraph_options.Rd @@ -7,7 +7,7 @@ \usage{ igraph_options(...) -igraph_opt(x, default = NULL) +igraph_opt(x, ..., default = NULL) } \arguments{ \item{\dots}{A list may be given as the only argument, or any number of diff --git a/man/shapes.Rd b/man/shapes.Rd index eea7a764c00..a06011fffee 100644 --- a/man/shapes.Rd +++ b/man/shapes.Rd @@ -14,7 +14,13 @@ shape_noclip(coords, el, params, end = c("both", "from", "to")) shape_noplot(coords, v = NULL, params) -add_shape(shape, clip = shape_noclip, plot = shape_noplot, parameters = list()) +add_shape( + shape, + ..., + clip = shape_noclip, + plot = shape_noplot, + parameters = list() +) } \arguments{ \item{shape}{Character scalar, name of a vertex shape. If it is @@ -24,6 +30,8 @@ vertex shapes are returned.} \item{coords, el, params, end, v}{See parameters of the clipping/plotting functions below.} +\item{...}{These dots are for future extensions and must be empty.} + \item{clip}{An R function object, the clipping function.} \item{plot}{An R function object, the plotting function.} diff --git a/man/tkplot.Rd b/man/tkplot.Rd index b7f841ccc82..f601f105f74 100644 --- a/man/tkplot.Rd +++ b/man/tkplot.Rd @@ -16,11 +16,11 @@ \usage{ tkplot(graph, canvas.width = 450, canvas.height = 450, ...) -tk_close(tkp.id, window.close = TRUE) +tk_close(tkp.id, ..., window.close = TRUE) tk_off() -tk_fit(tkp.id, width = NULL, height = NULL) +tk_fit(tkp.id, ..., width = NULL, height = NULL) tk_center(tkp.id) @@ -28,11 +28,11 @@ tk_reshape(tkp.id, newlayout, ..., params) tk_postscript(tkp.id) -tk_coords(tkp.id, norm = FALSE) +tk_coords(tkp.id, ..., norm = FALSE) tk_set_coords(tkp.id, coords) -tk_rotate(tkp.id, degree = NULL, rad = NULL) +tk_rotate(tkp.id, ..., degree = NULL, rad = NULL) tk_canvas(tkp.id) } diff --git a/tests/testthat/test-par.R b/tests/testthat/test-par.R index a0e6b78f609..560a60de9ac 100644 --- a/tests/testthat/test-par.R +++ b/tests/testthat/test-par.R @@ -56,3 +56,23 @@ test_that("with_igraph_opt works", { expect_true(igraph_opt("sparsematrices")) expect_true(inherits(res, "matrix")) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("igraph_opt() falls back to `default` for unset options", { + # An unset option falls back to the given default. + expect_identical(igraph_opt("no.such.option", default = 42), 42) + + # A set option wins over the default. + local_igraph_options(annotate.plot = TRUE) + expect_true(igraph_opt("annotate.plot", default = FALSE)) +}) + +test_that("igraph_opt() recovers a positional `default` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + + lifecycle::expect_deprecated( + res <- igraph_opt("no.such.option", 42) + ) + expect_identical(res, igraph_opt("no.such.option", default = 42)) +}) diff --git a/tests/testthat/test-plot.common.R b/tests/testthat/test-plot.common.R new file mode 100644 index 00000000000..69682f6f0d7 --- /dev/null +++ b/tests/testthat/test-plot.common.R @@ -0,0 +1,20 @@ +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("curve_multiple() spreads curvatures up to `start`", { + # Three parallel edges between the same vertex pair, plus a single edge. + g <- make_graph(c(1, 2, 1, 2, 1, 2, 3, 4)) + + # The bundle is spaced equally in [-start, start], single edges stay straight. + expect_equal(curve_multiple(g, start = 1), c(-1, 0, 1, 0)) + expect_equal(curve_multiple(g), c(-0.5, 0, 0.5, 0)) +}) + +test_that("curve_multiple() recovers a positional `start` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + g <- make_graph(c(1, 2, 1, 2, 1, 2, 3, 4)) + + lifecycle::expect_deprecated( + res <- curve_multiple(g, 1) + ) + expect_identical(res, curve_multiple(g, start = 1)) +}) diff --git a/tests/testthat/test-plot.shapes.R b/tests/testthat/test-plot.shapes.R index 37ea9ae1f13..a424f96b7cd 100644 --- a/tests/testthat/test-plot.shapes.R +++ b/tests/testthat/test-plot.shapes.R @@ -272,3 +272,43 @@ test_that("shape functions handle edge cases", { expect_silent(clip_func(coords, el, params_zero, "both")) } }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("add_shape() covers `clip`, `plot` and `parameters`", { + clip_cov <- function(coords, el, params, end = c("both", "from", "to")) { + coords + } + plot_cov <- function(coords, v = NULL, params) invisible(NULL) + + expect_true(add_shape( + "coverage_shape", + clip = clip_cov, + plot = plot_cov, + parameters = list(vertex.coverage.dummy = 42) + )) + # The clip/plot functions are registered under the new shape name. + expect_identical( + shapes("coverage_shape"), + list(clip = clip_cov, plot = plot_cov) + ) + # The extra parameter is installed as an igraph option with its default. + expect_identical(igraph_opt("vertex.coverage.dummy"), 42) +}) + +test_that("add_shape() recovers a positional `clip` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + clip_cov <- function(coords, el, params, end = c("both", "from", "to")) { + coords + } + + lifecycle::expect_deprecated( + res <- add_shape("coverage_shape_pos", clip_cov) + ) + expect_true(res) + # The positional value lands on `clip`; `plot` keeps its default. + expect_identical( + shapes("coverage_shape_pos"), + list(clip = clip_cov, plot = shape_noplot) + ) +}) diff --git a/tests/testthat/test-tkplot.R b/tests/testthat/test-tkplot.R new file mode 100644 index 00000000000..b86df647d9c --- /dev/null +++ b/tests/testthat/test-tkplot.R @@ -0,0 +1,58 @@ +# tkplot() needs tcltk and an interactive Tk window, which headless test runs +# cannot open. +# The keyword-only arguments of the tk_*() helpers are exercised through the +# plot-ID lookup, which fails for an unknown ID before any Tk interaction. + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("tk_close() covers `window.close` by name", { + # `window.close = FALSE` skips Tk entirely; removing the unknown ID warns. + expect_warning(tk_close(9999, window.close = FALSE), "not found") +}) + +test_that("tk_close() recovers a positional `window.close` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + + # The deprecation fires during argument recovery, before the ID lookup warns. + lifecycle::expect_deprecated( + expect_warning(tk_close(9999, FALSE), "not found") + ) +}) + +test_that("tk_fit() covers `width` and `height` by name", { + expect_error(tk_fit(9999, width = 400, height = 300), "not found") +}) + +test_that("tk_fit() recovers positional `width`/`height` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + + # The deprecation fires during argument recovery, before the ID lookup fails. + lifecycle::expect_deprecated( + expect_error(tk_fit(9999, 400, 300), "not found") + ) +}) + +test_that("tk_coords() covers `norm` by name", { + expect_error(tk_coords(9999, norm = TRUE), "not found") +}) + +test_that("tk_coords() recovers a positional `norm` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + + lifecycle::expect_deprecated( + expect_error(tk_coords(9999, TRUE), "not found") + ) +}) + +test_that("tk_rotate() covers `degree` and `rad` by name", { + expect_error(tk_rotate(9999, degree = 90), "not found") + expect_error(tk_rotate(9999, rad = pi / 2), "not found") +}) + +test_that("tk_rotate() recovers a positional `degree` with a deprecation", { + rlang::local_options(lifecycle_verbosity = "warning") + + lifecycle::expect_deprecated( + expect_error(tk_rotate(9999, 90), "not found") + ) +}) diff --git a/tools/migrations/plotting.R b/tools/migrations/plotting.R new file mode 100644 index 00000000000..876a80666f8 --- /dev/null +++ b/tools/migrations/plotting.R @@ -0,0 +1,79 @@ +# Argument-signature migrations: plotting +# Schema: see tools/migrations/README.md. Regenerate with: +# Rscript tools/generate-migrations.R + +migrations <- list( + igraph_opt = list( + old = function(x, default) {}, + new = function( + x, + ..., + default = NULL + ) {}, + when = "3.0.0" + ), + + curve_multiple = list( + old = function(graph, start) {}, + new = function( + graph, + ..., + start = 0.5 + ) {}, + when = "3.0.0" + ), + + add_shape = list( + old = function(shape, clip, plot, parameters) {}, + new = function( + shape, + ..., + clip = shape_noclip, + plot = shape_noplot, + parameters = list() + ) {}, + when = "3.0.0" + ), + + tk_close = list( + old = function(tkp.id, window.close) {}, + new = function( + tkp.id, + ..., + window.close = TRUE + ) {}, + when = "3.0.0" + ), + + tk_coords = list( + old = function(tkp.id, norm) {}, + new = function( + tkp.id, + ..., + norm = FALSE + ) {}, + when = "3.0.0" + ), + + tk_fit = list( + old = function(tkp.id, width, height) {}, + new = function( + tkp.id, + ..., + width = NULL, + height = NULL + ) {}, + when = "3.0.0" + ), + + tk_rotate = list( + old = function(tkp.id, degree, rad) {}, + new = function( + tkp.id, + ..., + degree = NULL, + rad = NULL + ) {}, + when = "3.0.0" + ) +)