diff --git a/NEWS.md b/NEWS.md index a24bf34615d..3a9a942edeb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -899,9 +899,6 @@ - `layout.kamada.kawai()` is now deprecated with a warning instead of a message. - `layout.lgl()` is now deprecated with a warning instead of a message. - `layout_with_mds()` is now deprecated with a warning instead of a message. -- `layout.spring()` is now defunct (errors instead of warning). -- `layout.svd()` is now defunct (errors instead of warning). -- `layout.fruchterman.reingold.grid()` is now defunct (errors instead of warning). - `layout.drl()` is now deprecated with a warning instead of a message. - `nei()` has been removed. - `innei()` has been removed. diff --git a/R/attributes.R b/R/attributes.R index 13824155c1c..2a19c5ecaa3 100644 --- a/R/attributes.R +++ b/R/attributes.R @@ -1327,6 +1327,12 @@ igraph.i.attribute.combination <- function(comb, allow_rename = FALSE) { "Attribute combination element must be a function or character scalar." ) } + # A plain loop, not lapply(): check_deprecated_function() reads the call + # stack to find out who igraph was called by, and a functional in between + # would hide the caller behind a frame of its own. + for (element in comb) { + check_deprecated_function(element) + } if (is.null(names(comb))) { names(comb) <- rep("", length(comb)) } diff --git a/R/cliques.R b/R/cliques.R index 29fefbe4a65..43865d3cb28 100644 --- a/R/cliques.R +++ b/R/cliques.R @@ -257,6 +257,7 @@ clique.number <- function(graph) { cliques <- function(graph, min = NULL, max = NULL, ..., callback = NULL) { ensure_igraph(graph) check_dots_empty() + check_deprecated_function(callback) if (is.null(callback)) { # Collector mode: use original implementation @@ -308,6 +309,7 @@ max_cliques <- function( ) { ensure_igraph(graph) check_dots_empty() + check_deprecated_function(callback) # Handle file and subset modes (original functionality) if (!is.null(file)) { diff --git a/R/community.R b/R/community.R index 783f7676000..3e0edfb13b9 100644 --- a/R/community.R +++ b/R/community.R @@ -2562,6 +2562,7 @@ cluster_leading_eigen <- function( # Argument checks ensure_igraph(graph) + check_deprecated_function(callback) steps <- as.numeric(steps) if (is.null(weights) && "weight" %in% edge_attr_names(graph)) { diff --git a/R/cycles.R b/R/cycles.R index 6859cdf1f3f..e2587465334 100644 --- a/R/cycles.R +++ b/R/cycles.R @@ -166,6 +166,7 @@ simple_cycles <- function( # Argument checks ensure_igraph(graph) check_dots_empty() + check_deprecated_function(callback) if (is.null(callback)) { simple_cycles_impl( diff --git a/R/layout.R b/R/layout.R index 5c028fe8959..1f803bc0808 100644 --- a/R/layout.R +++ b/R/layout.R @@ -1181,6 +1181,7 @@ layout_nicely <- function(graph, dim = 2, ...) { lay <- graph_attr(graph, "layout") if (is.function(lay)) { if (!identical(lay, layout_nicely)) { + check_deprecated_function(lay) return(lay(graph, ...)) } else { # nop, we'll deal with it later below @@ -3409,6 +3410,7 @@ layout_components <- function(graph, layout = NULL, ...) { if (is.null(layout)) { layout <- layout_with_kk } + check_deprecated_function(layout) V(graph)$id <- seq(vcount(graph)) gl <- decompose(graph) @@ -3433,7 +3435,8 @@ layout_components <- function(graph, layout = NULL, ...) { #' @export #' @keywords internal layout.spring <- function(graph, ...) { - lifecycle::deprecate_stop("2.1.0", "layout.spring()", "layout_with_fr()") + lifecycle::deprecate_warn("2.1.0", "layout.spring()", "layout_with_fr()") + layout_with_fr(graph) } #' SVD layout, this was removed from igraph @@ -3450,7 +3453,8 @@ layout.spring <- function(graph, ...) { #' @keywords internal #' @export layout.svd <- function(graph, ...) { - lifecycle::deprecate_stop("2.1.0", "layout.svd()", "layout_with_fr()") + lifecycle::deprecate_warn("2.1.0", "layout.svd()", "layout_with_fr()") + layout_with_fr(graph) } #' Grid Fruchterman-Reingold layout, this was removed from igraph @@ -3468,11 +3472,12 @@ layout.svd <- function(graph, ...) { #' @keywords internal #' @export layout.fruchterman.reingold.grid <- function(graph, ...) { - lifecycle::deprecate_stop( + lifecycle::deprecate_warn( "2.1.0", "layout.fruchterman.reingold.grid()", "layout_with_fr()" ) + layout_with_fr(graph) } #' The DrL graph layout generator diff --git a/R/motifs.R b/R/motifs.R index 97a526b2905..b026131caf2 100644 --- a/R/motifs.R +++ b/R/motifs.R @@ -221,6 +221,7 @@ motifs <- function( if (!is.null(cut.prob) && length(cut.prob) != size) { cli::cli_abort("{.arg cut.prob} must be the same length as {.arg size}") } + check_deprecated_function(callback) # If callback is provided, use the callback implementation if (!is.null(callback)) { diff --git a/R/plot.common.R b/R/plot.common.R index 9fb2c5783f6..bb87cbce48a 100644 --- a/R/plot.common.R +++ b/R/plot.common.R @@ -552,6 +552,7 @@ i.parse.plot.params <- function(graph, params) { ret <- function() { v <- p[[type]][[name]] if (is.function(v) && !dontcall) { + check_deprecated_function(v) v <- v(graph) } if (is.null(range)) { diff --git a/R/plot.shapes.R b/R/plot.shapes.R index dc1eb672675..3e6d1708c80 100644 --- a/R/plot.shapes.R +++ b/R/plot.shapes.R @@ -459,6 +459,9 @@ add_shape <- function( )) } + check_deprecated_function(clip) + check_deprecated_function(plot) + assign(shape, value = list(clip = clip, plot = plot), envir = .igraph.shapes) do.call(igraph_options, parameters) invisible(TRUE) diff --git a/R/printr.R b/R/printr.R index 7f5294859a8..7804d1cf7c6 100644 --- a/R/printr.R +++ b/R/printr.R @@ -35,6 +35,7 @@ printer_callback <- function(fun) { if (!is.function(fun)) { warning("'fun' is not a function") } + check_deprecated_function(fun) add_class(fun, "printer_callback") } @@ -58,7 +59,12 @@ print_footer <- function(footer) { } print_head_foot <- function(head_foot) { - if (is.function(head_foot)) head_foot() else cat(head_foot) + if (is.function(head_foot)) { + check_deprecated_function(head_foot) + head_foot() + } else { + cat(head_foot) + } } #' Print the only the head of an R object @@ -199,6 +205,7 @@ indent_print <- function(..., .indent = " ", .printer = NULL) { if (is.null(.printer)) { .printer <- print } + check_deprecated_function(.printer) if (length(.indent) != 1 || !is.character(.indent)) { indent <- .indent # cli literal cannot start with a dot diff --git a/R/scan.R b/R/scan.R index a719650c8d1..8370c55d767 100644 --- a/R/scan.R +++ b/R/scan.R @@ -130,6 +130,7 @@ local_scan <- function( stopifnot( is.null(FUN) || is.function(FUN) || (is.character(FUN) && length(FUN) == 1) ) + check_deprecated_function(FUN) ## Logical stopifnot(is.logical(weighted), length(weighted) == 1) diff --git a/R/structural-properties.R b/R/structural-properties.R index c2970ebc5a9..4fd56c9f653 100644 --- a/R/structural-properties.R +++ b/R/structural-properties.R @@ -3795,6 +3795,7 @@ bfs <- function( } if (!is.null(callback)) { callback <- as.function(callback) + check_deprecated_function(callback) } on.exit(.Call(Rx_igraph_finalizer)) @@ -4058,9 +4059,11 @@ dfs <- function( unreachable <- as.logical(unreachable) if (!is.null(in.callback)) { in.callback <- as.function(in.callback) + check_deprecated_function(in.callback) } if (!is.null(out.callback)) { out.callback <- as.function(out.callback) + check_deprecated_function(out.callback) } on.exit(.Call(Rx_igraph_finalizer)) diff --git a/R/tkplot.R b/R/tkplot.R index 9342a284939..e7297045886 100644 --- a/R/tkplot.R +++ b/R/tkplot.R @@ -700,6 +700,7 @@ tk_center <- function(tkp.id) { #' @export tk_reshape <- function(tkp.id, newlayout, ..., params) { # nocov start + check_deprecated_function(newlayout) tkp <- .tkplot.get(tkp.id) new_coords <- do_call( newlayout, diff --git a/R/topology.R b/R/topology.R index 1069c076de5..3c4745508fd 100644 --- a/R/topology.R +++ b/R/topology.R @@ -1060,6 +1060,7 @@ isomorphisms <- function(graph1, graph2, method = "vf2", ..., callback = NULL) { "Only {.arg method} = {.val vf2} is currently supported." ) } + check_deprecated_function(callback) if (is.null(callback)) { graph.get.isomorphisms.vf2(graph1, graph2, ...) @@ -1191,6 +1192,7 @@ subgraph_isomorphisms <- function( "Callback parameter is only supported for {.arg method} = {.val vf2}." ) } + check_deprecated_function(callback) if (method == "lad") { graph.subisomorphic.lad(pattern, target, all.maps = TRUE, ...)$maps diff --git a/R/utils-deprecated.R b/R/utils-deprecated.R new file mode 100644 index 00000000000..5a33831b21e --- /dev/null +++ b/R/utils-deprecated.R @@ -0,0 +1,215 @@ +# Signal the deprecation of a function that was passed as an argument. +# +# Many igraph functions take another function as an argument and call it on the +# user's behalf, for instance the `layout` argument of `plot.igraph()` or the +# `clip` and `plot` arguments of `add_shape()`. +# lifecycle attributes a deprecation to whoever called the deprecated function, +# and that caller is igraph itself here, not the user: `deprecate_soft()` +# therefore stays silent, and `deprecate_warn()` blames igraph and asks the +# user to report a bug against it. +# Either way the user never learns that the function they passed is deprecated, +# until the deprecation becomes hard and the call fails outright. +# +# `check_deprecated_function()` inspects `fn` before it is called and replays +# its deprecation against the environment igraph was called from, so that a +# deprecated function is reported the same way whether the user calls it +# themselves or hands it to igraph. +# +# The deprecation is replayed with the same signaller and the same message as +# the original, which keeps lifecycle's deduplication working: the deprecation +# `fn` signals when it is subsequently called is recognized as the one already +# reported here, and is not repeated. +# +# Anything but a deprecated function, `NULL` included, is left alone. +# Returns `fn`, invisibly. +check_deprecated_function <- function(fn) { + deprecation <- function_deprecation(fn) + + if (!is.null(deprecation)) { + replay_deprecation(deprecation, env = environment(fn)) + } + + invisible(fn) +} + +# The deprecation a function signals for itself, as the `signaller` name and +# the `when`, `what`, `with` and `details` arguments it passes, or NULL if `fn` +# is not a deprecated function. +# +# Only unconditional, function-level deprecations are found, that is, a +# `lifecycle::deprecate_*()` call among the statements of the body of `fn` +# whose `what` names `fn` itself. A deprecation nested deeper in the body is +# conditional on the arguments of the call -- it deprecates an argument or one +# of its values, not the function -- and only the call itself can tell whether +# it applies. +function_deprecation <- function(fn) { + if (!is.function(fn) || is.primitive(fn)) { + return(NULL) + } + + # Functions that live outside a package cannot be deprecated with lifecycle: + # it derives the package to name in the message from the environment. + ns <- topenv(environment(fn)) + if (!isNamespace(ns)) { + return(NULL) + } + + for (statement in body_statements(fn)) { + deprecation <- as_deprecation(statement) + if (!is.null(deprecation) && deprecates(deprecation, fn, ns)) { + return(deprecation) + } + } + + NULL +} + +body_statements <- function(fn) { + body <- body(fn) + if (rlang::is_call(body, "{")) as.list(body)[-1] else list(body) +} + +deprecation_signallers <- c( + "deprecate_soft", + "deprecate_warn", + "deprecate_stop" +) + +# The deprecation described by a call, or NULL if it is not a +# `lifecycle::deprecate_*()` call with constant arguments. +as_deprecation <- function(statement) { + if (!is.call(statement)) { + return(NULL) + } + + signaller <- statement[[1]] + if (rlang::is_call(signaller, "::")) { + if (!identical(signaller[[2]], quote(lifecycle))) { + return(NULL) + } + signaller <- signaller[[3]] + } + if (!rlang::is_symbol(signaller)) { + return(NULL) + } + + signaller <- rlang::as_string(signaller) + if (!signaller %in% deprecation_signallers) { + return(NULL) + } + + # A call that does not fit the signature is none of lifecycle's, whatever it + # is named. + statement <- tryCatch( + match.call( + definition = utils::getFromNamespace(signaller, "lifecycle"), + call = statement + ), + error = function(e) NULL + ) + if (is.null(statement)) { + return(NULL) + } + + arguments <- lapply( + stats::setNames(nm = c("when", "what", "with", "details")), + function(name) constant_value(statement[[name]]) + ) + # A deprecation whose message is assembled at run time cannot be replayed + # from the body alone. + if (any(vapply(arguments, identical, logical(1), y = not_constant))) { + return(NULL) + } + + c(list(signaller = signaller), arguments) +} + +# Distinguishable from every constant, so that an argument that is absent +# (`with` and `details` are optional) is not mistaken for one that could not be +# evaluated. +not_constant <- new.env(parent = emptyenv()) + +constant_value <- function(expr) { + # Evaluating in the base environment resolves literals and calls such as + # `c("one", "other")`, and fails for anything that depends on the state of + # the deprecated function. + tryCatch(eval(expr, baseenv()), error = function(e) not_constant) +} + +# Does a deprecation found in the body of `fn` deprecate `fn` as a whole? +deprecates <- function(deprecation, fn, ns) { + what <- deprecation$what + if (!rlang::is_string(what)) { + return(FALSE) + } + + # Function-level deprecations spell the function out as a call without + # arguments, e.g. "layout.circle()"; argument-level ones name the argument + # inside the parentheses, e.g. "bfs(father = )". + if (!grepl("^[^(]+\\(\\)$", what)) { + return(FALSE) + } + name <- sub("\\(\\)$", "", what) + name <- sub("^.*::", "", name) + + # Guard against a wrapper that signals the deprecation of some *other* + # function, e.g. of the one it is the replacement for. + deprecated <- get0(name, envir = ns, inherits = FALSE) + is.null(deprecated) || identical(deprecated, fn) +} + +replay_deprecation <- function(deprecation, env) { + user_env <- igraph_user_env() + + switch( + deprecation$signaller, + deprecate_soft = lifecycle::deprecate_soft( + when = deprecation$when, + what = deprecation$what, + with = deprecation$with, + details = deprecation$details, + env = env, + user_env = user_env + ), + deprecate_warn = lifecycle::deprecate_warn( + when = deprecation$when, + what = deprecation$what, + with = deprecation$with, + details = deprecation$details, + env = env, + user_env = user_env + ), + deprecate_stop = lifecycle::deprecate_stop( + when = deprecation$when, + what = deprecation$what, + with = deprecation$with, + details = deprecation$details, + env = env + ) + ) +} + +# The environment igraph was called from: the innermost caller that does not +# belong to igraph itself. +# +# lifecycle needs it to tell a deprecation the user is responsible for from one +# that happens behind their back, and to name the package to blame in the +# latter case. Walking the caller chain keeps this independent of how deeply a +# deprecated argument is passed on inside igraph before it is looked at, which +# a fixed `rlang::caller_env(n)` would have to track. Callers must not reach +# the check through a functional such as `lapply()`, whose frame would end the +# walk in place of the user's. +igraph_user_env <- function() { + ns <- topenv(environment(igraph_user_env)) + + generation <- 1L + repeat { + env <- parent.frame(generation) + # parent.frame() bottoms out at the global environment, so the walk + # terminates there even if every frame belongs to igraph. + if (identical(env, globalenv()) || !identical(topenv(env), ns)) { + return(env) + } + generation <- generation + 1L + } +} diff --git a/man/layout.fruchterman.reingold.grid.Rd b/man/layout.fruchterman.reingold.grid.Rd index 67368c0ed9a..c9bd2d65c89 100644 --- a/man/layout.fruchterman.reingold.grid.Rd +++ b/man/layout.fruchterman.reingold.grid.Rd @@ -19,4 +19,8 @@ Layout coordinates, a two column matrix. Now it calls the Fruchterman-Reingold layout \code{\link[=layout_with_fr]{layout_with_fr()}}. } +\section{Related documentation in the C library}{ +\href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +} + \keyword{internal} diff --git a/man/layout.spring.Rd b/man/layout.spring.Rd index b87db5942d6..b876cffadcd 100644 --- a/man/layout.spring.Rd +++ b/man/layout.spring.Rd @@ -19,4 +19,8 @@ Layout coordinates, a two column matrix. Now it calls the Fruchterman-Reingold layout \code{\link[=layout_with_fr]{layout_with_fr()}}. } +\section{Related documentation in the C library}{ +\href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +} + \keyword{internal} diff --git a/man/layout.svd.Rd b/man/layout.svd.Rd index c83ee8cc0a7..08f44ed6d11 100644 --- a/man/layout.svd.Rd +++ b/man/layout.svd.Rd @@ -19,4 +19,8 @@ Layout coordinates, a two column matrix. Now it calls the Fruchterman-Reingold layout \code{\link[=layout_with_fr]{layout_with_fr()}}. } +\section{Related documentation in the C library}{ +\href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +} + \keyword{internal} diff --git a/tests/testthat/_snaps/utils-deprecated.md b/tests/testthat/_snaps/utils-deprecated.md new file mode 100644 index 00000000000..f43315d6268 --- /dev/null +++ b/tests/testthat/_snaps/utils-deprecated.md @@ -0,0 +1,9 @@ +# check_deprecated_function() reports a defunct function + + Code + check_deprecated_function(hub.score) + Condition + Error: + ! `hub.score()` was deprecated in igraph 2.0.0 and is now defunct. + i Please use `hits_scores()` instead. + diff --git a/tests/testthat/test-utils-deprecated.R b/tests/testthat/test-utils-deprecated.R new file mode 100644 index 00000000000..eedd1eff75c --- /dev/null +++ b/tests/testthat/test-utils-deprecated.R @@ -0,0 +1,261 @@ +# Call `fn(...)` the way a user would. +# +# Two things stand between a test and what a user sees. The environment a test +# runs in belongs to igraph, and lifecycle exempts the test suite of the +# deprecating package from its "is this the user's doing?" question, treating +# igraph's own frames as direct (see setup-lifecycle.R). Both would make these +# tests pass whether or not the check reports anything. So call from an +# environment that belongs to no package, with the exemption cleared. +as_user <- function(fn, ...) { + withr::local_envvar(TESTTHAT_PKG = "") + user <- rlang::new_function( + args = rlang::pairlist2(... = ), + body = quote(fn(...)), + env = rlang::env(globalenv(), fn = fn) + ) + user(...) +} + +# The deprecated function warns again when igraph goes on to call it, unless +# lifecycle recognizes the repeat. Capturing every warning keeps a second one +# from escaping the test, and asserts that nothing else did. +expect_deprecation <- function(expr, regexp) { + warnings <- testthat::capture_warnings(expr) + expect_gt(length(warnings), 0) + expect_match(warnings, regexp, all = TRUE) + # The point of the exercise: lifecycle appends this to a deprecation it holds + # igraph responsible for, and asks the user to report a bug about it. + expect_no_match(warnings, "likely used in") +} + +test_that("function_deprecation() reads the deprecation a function signals", { + expect_equal( + function_deprecation(layout.circle), + list( + signaller = "deprecate_warn", + when = "2.1.0", + what = "layout.circle()", + with = "layout_in_circle()", + details = NULL + ) + ) +}) + +test_that("function_deprecation() ignores anything that is not a deprecated function", { + expect_null(function_deprecation(layout_in_circle)) + expect_null(function_deprecation(NULL)) + expect_null(function_deprecation("layout.circle")) + expect_null(function_deprecation(sum)) + expect_null(function_deprecation(function(graph) NULL)) +}) + +test_that("function_deprecation() ignores deprecations of an argument", { + # bfs() deprecates its `father` argument, conditionally on it being passed; + # bfs() itself is not deprecated. + expect_null(function_deprecation(bfs)) +}) + +test_that("check_deprecated_function() returns its argument invisibly", { + expect_invisible(out <- check_deprecated_function(layout_in_circle)) + expect_identical(out, layout_in_circle) +}) + +test_that("check_deprecated_function() reports a deprecated function", { + lifecycle::expect_deprecated( + check_deprecated_function(layout.circle), + "layout_in_circle" + ) +}) + +test_that("check_deprecated_function() reports a defunct function", { + expect_snapshot(error = TRUE, { + check_deprecated_function(hub.score) + }) +}) + +test_that("igraph_user_env() skips igraph's own frames", { + igraph_frame <- function() igraph_user_env() + environment(igraph_frame) <- asNamespace("igraph") + + caller <- environment() + expect_identical(igraph_frame(), caller) +}) + +# ---- deprecated functions passed as arguments ------------------------- + +test_that("plot() reports a deprecated layout function", { + g <- make_ring(5) + withr::local_pdf(NULL) + + expect_deprecation( + as_user(plot, g, layout = layout.circle), + "layout_in_circle" + ) +}) + +test_that("plot() reports a deprecated layout graph attribute", { + g <- make_ring(5) + g$layout <- layout.random + withr::local_pdf(NULL) + + expect_deprecation(as_user(plot, g), "layout_randomly") +}) + +test_that("plot() is silent about a current layout function", { + g <- make_ring(5) + withr::local_pdf(NULL) + + expect_no_condition(plot(g, layout = layout_in_circle)) +}) + +test_that("layout_nicely() reports a deprecated layout graph attribute", { + g <- make_ring(5) + g$layout <- layout.circle + + expect_deprecation(as_user(layout_nicely, g), "layout_in_circle") +}) + +test_that("layout_components() reports a deprecated layout function", { + g <- make_ring(5) + make_ring(4) + + expect_deprecation( + as_user(layout_components, g, layout.circle), + "layout_in_circle" + ) +}) + +test_that("add_shape() reports deprecated clip and plot functions", { + expect_deprecation( + as_user(add_shape, "test-clip", clip = igraph.shape.noclip), + "shape_noclip" + ) + expect_deprecation( + as_user(add_shape, "test-plot", plot = igraph.shape.noplot), + "shape_noplot" + ) +}) + +test_that("local_scan() reports a deprecated FUN", { + g <- make_ring(5) + + expect_deprecation( + as_user(local_scan, g, FUN = graph.density), + "edge_density" + ) +}) + +test_that("attribute combinations report a deprecated function", { + g <- make_graph(c(1, 2, 1, 2)) + E(g)$weight <- c(1, 2) + + # `is.igraph()` is not a meaningful combiner, but it is softly deprecated and + # accepts the vector of attribute values that a combiner is handed. + expect_deprecation( + as_user(simplify, g, edge.attr.comb = list(weight = is.igraph)), + "is_igraph" + ) +}) + +test_that("callbacks report a deprecated function", { + # A graph without cliques, so that the callback is reported but never called. + g <- make_empty_graph(0) + + expect_deprecation( + as_user(cliques, g, callback = graph.density), + "edge_density" + ) +}) + +# ---- deprecation levels are preserved ---------------------------------- + +# A function that igraph deprecates, without the noise of one that also does +# something. Its name is what tells lifecycle's deduplication one deprecation +# apart from the other, so each test needs its own. +deprecated_igraph_function <- function(what, signaller = "deprecate_soft") { + rlang::new_function( + args = rlang::pairlist2(... = ), + body = rlang::call2(signaller, "2.0.0", what, .ns = "lifecycle"), + env = asNamespace("igraph") + ) +} + +# An igraph function that calls a function argument, with and without the check +# under test. +igraph_function <- function(checked) { + fn <- if (checked) { + function(callback, ...) { + check_deprecated_function(callback) + callback(...) + } + } else { + function(callback, ...) callback(...) + } + environment(fn) <- asNamespace("igraph") + fn +} + +test_that("a soft deprecation reaches the user that caused it", { + # Silent as things stand: lifecycle sees an igraph frame as the caller, and + # does not report a package's own use of a softly deprecated function. + expect_no_warning( + as_user(igraph_function(FALSE), deprecated_igraph_function("soft_silent()")) + ) + + expect_deprecation( + as_user(igraph_function(TRUE), deprecated_igraph_function("soft_heard()")), + "soft_heard" + ) +}) + +test_that("a soft deprecation stays silent for another package", { + fn <- deprecated_igraph_function("soft_other_package()") + call_igraph <- function() igraph_function(TRUE)(fn) + environment(call_igraph) <- rlang::env( + asNamespace("stats"), + fn = fn, + igraph_function = igraph_function + ) + + withr::local_envvar(TESTTHAT_PKG = "") + expect_no_warning(call_igraph()) +}) + +test_that("a warning deprecation stops blaming igraph for the user's choice", { + blamed <- testthat::capture_warnings( + as_user( + igraph_function(FALSE), + deprecated_igraph_function("warn_blamed()", "deprecate_warn") + ) + ) + expect_match(blamed, "likely used in the .*igraph.* package") + + # Reported once, not twice: the check replays the deprecation of the function + # verbatim, so lifecycle recognizes the one the function itself signals when + # it is called next as a repeat. + attributed <- testthat::capture_warnings( + as_user( + igraph_function(TRUE), + deprecated_igraph_function("warn_attributed()", "deprecate_warn") + ) + ) + expect_length(attributed, 1) + expect_no_match(attributed, "likely used in") +}) + +# ---- un-deprecated layout callbacks ------------------------------------ + +test_that("the layout functions that plot() may be handed still work", { + g <- make_ring(5) + + expect_deprecation(coords <- layout.spring(g), "layout_with_fr") + expect_equal(dim(coords), c(5, 2)) + + expect_deprecation(coords <- layout.svd(g), "layout_with_fr") + expect_equal(dim(coords), c(5, 2)) + + expect_deprecation( + coords <- layout.fruchterman.reingold.grid(g), + "layout_with_fr" + ) + expect_equal(dim(coords), c(5, 2)) +})