Skip to content

fix: report a deprecated function passed as an argument, and un-deprecate the layout callbacks - #2844

Closed
krlmlr wants to merge 5 commits into
mainfrom
claude/deprecated-callback-functions-87kfjp
Closed

fix: report a deprecated function passed as an argument, and un-deprecate the layout callbacks#2844
krlmlr wants to merge 5 commits into
mainfrom
claude/deprecated-callback-functions-87kfjp

Conversation

@krlmlr

@krlmlr krlmlr commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

This PR was prepared with Claude Code (Claude Opus).

#2845 is an alternative to this PR, closing the same gap by changing where the callback is called instead of inspecting it. Only one of the two should land; that PR's description compares them.

The problem

igraph functions that take another function as an argument call it on the user's behalf. lifecycle attributes a deprecation to whoever called the deprecated function, and that caller is igraph, not the user. So plot(g, layout = layout.circle) was silent, while layout.circle(g) warned:

  • deprecate_soft() stays quiet entirely, because the call does not come from the global environment;
  • deprecate_warn() does warn, but appends "The deprecated feature was likely used in the igraph package. Please report the issue at …", pointing the user at our issue tracker for something they wrote themselves.

Either way the user is not told that the function they passed is on its way out, until the deprecation becomes hard and the call fails outright. setup-lifecycle.R already flags this class of deprecation in our own test suite (#2782); this closes the same gap for users.

The check

check_deprecated_function() reads the deprecation off the function object — the lifecycle::deprecate_*() call among the statements of its body whose what names the function itself — and replays it against the environment igraph was called from, using the same signaller and the same message.

Replaying verbatim is what keeps this from doubling up: lifecycle deduplicates by message, so the deprecation the function goes on to signal when igraph calls it is recognized as the one already reported, and stays silent. Replaying with the original signaller also preserves the deprecation level, so a soft deprecation still only reaches the user who caused it, and a defunct function still errors.

A deprecation nested deeper in the body is conditional on the arguments of the call — it deprecates an argument or one of its values, like bfs(father = ), not the function — and is left to the call itself.

Every argument that takes a function now goes through the check:

  • plotting parameters, which covers plot(), tkplot(), rglplot(), the layout graph attribute and function-valued igraph_options();
  • layout_nicely(), layout_components() and tk_reshape();
  • the clip and plot functions of add_shape();
  • attribute combinations, so simplify(), contract(), union() and friends;
  • local_scan(FUN = ) and through it scan_stat();
  • printer callbacks;
  • the search callbacks of bfs(), dfs(), cliques(), max_cliques(), motifs(), simple_cycles(), isomorphisms(), subgraph_isomorphisms() and cluster_leading_eigen().
plot(make_ring(5), layout = layout.circle)
#> Warning: `layout.circle()` was deprecated in igraph 2.1.0.
#> ℹ Please use `layout_in_circle()` instead.

Un-deprecating the layout callbacks

layout.spring(), layout.svd() and layout.fruchterman.reingold.grid() were made defunct in #2634. That change is on main only — neither 2.3.2 nor 2.3.3 shipped it — and these are exactly the functions users hand to plot(layout = ), where the deprecation warning has never reached them. Making them error before they have ever warned in that position breaks working code without notice.

They now warn again and lay out with layout_with_fr(), as in 2.3.3. Their documentation already described that behaviour. The three NEWS bullets announcing the hard deprecation are dropped, since they are in an unreleased section and no longer describe what happens.

Not touched, for the record: layout.grid.3d() was removed in the same PR, but it was already defunct in 2.1.0 and in every released 2.3.x, so no working code depended on it. It is worth deciding separately whether a removed callback should keep a defunct stub for the sake of the error message.

Testing

New tests/testthat/test-utils-deprecated.R covers the detection, the replay at each level, the deduplication, the argument sites above, and that the three layout functions work again.

Getting the tests to mean anything took some care: they run in an environment that belongs to igraph, and lifecycle exempts the test suite of the deprecating package from its "is this the user's doing?" question, so a naive test passes with or without the change. They call through an as_user() helper — an environment that belongs to no package, with TESTTHAT_PKG cleared — and assert both that the deprecation is reported and that it does not carry lifecycle's "likely used in the igraph package, please report the issue" footer.

Full suite passes; the two failures seen locally are environmental (a blocked download in test-foreign.R, and a callr subprocess in test-other.R that needs an installed igraph).

  • By submitting this pull request, I assign the copyright of my contribution to The igraph development team.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJx1uH2GvN5DcqdVzDLvwW

claude and others added 3 commits August 16, 2026 09:45
igraph functions that take another function as an argument call it on the
user's behalf.
lifecycle attributes a deprecation to whoever called the deprecated function,
and that caller is igraph, not the user:
`deprecate_soft()` stays silent, and `deprecate_warn()` blames igraph and asks
the user to report a bug against it.
Either way `plot(g, layout = layout.circle)` never tells the user that the
layout function they passed is deprecated,
until the deprecation becomes hard and the call fails outright.

`check_deprecated_function()` reads the deprecation off the function object and
replays it against the environment igraph was called from,
with the same signaller and the same message,
so lifecycle recognizes the deprecation the function goes on to signal as the
one already reported and does not repeat it.
Every argument that takes a function now goes through the check:
plotting parameters, layouts, vertex shapes, attribute combinations, scan
statistics, printer callbacks and the search callbacks.

`layout.spring()`, `layout.svd()` and `layout.fruchterman.reingold.grid()` are
deprecated with a warning again, and lay out with `layout_with_fr()` as before.
They are layout callbacks, so their hard deprecation -- made after 2.3.3 and
never released -- would break `plot(layout = )` for users who were never warned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJx1uH2GvN5DcqdVzDLvwW
@github-actions

Copy link
Copy Markdown
Contributor

This is how benchmark results would change (along with a 95% confidence interval in relative change) if b415cbe is merged into main:

  • 🚀as_adjacency_matrix: 806ms -> 800ms [-1.55%, -0.01%]
  • ✔️as_biadjacency_matrix: 814ms -> 809ms [-1.16%, +0.08%]
  • ✔️as_data_frame_both: 1.72ms -> 1.7ms [-3.49%, +0.56%]
  • ✔️as_long_data_frame: 4.26ms -> 4.25ms [-2.63%, +2.08%]
  • 🚀es_attr_filter: 2.88ms -> 2.82ms [-3.51%, -0.72%]
  • ✔️graph_from_adjacency_matrix: 150ms -> 150ms [-1.03%, +1.51%]
  • ✔️graph_from_data_frame: 4.04ms -> 3.91ms [-9.26%, +2.99%]
  • ✔️vs_attr_filter: 1.65ms -> 1.64ms [-2.73%, +2.48%]
  • ✔️vs_by_name: 1.07ms -> 1.07ms [-2.67%, +3.82%]
    Further explanation regarding interpretation and methodology can be found in the documentation.

claude added 2 commits August 16, 2026 13:38
The tests ran in an environment that belongs to igraph, and lifecycle exempts
the test suite of the deprecating package from its "is this the user's doing?"
question.
Both made the tests pass whether or not the check reported anything.

Call from an environment that belongs to no package, with the exemption
cleared, and assert that the deprecation does not carry lifecycle's
"likely used in the igraph package, please report the issue" footer -- which is
what a user sees today, and the point of the exercise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJx1uH2GvN5DcqdVzDLvwW
…ions-87kfjp' into claude/deprecated-callback-functions-87kfjp
@github-actions

Copy link
Copy Markdown
Contributor

This is how benchmark results would change (along with a 95% confidence interval in relative change) if e5cec1c is merged into main:

  • ✔️as_adjacency_matrix: 752ms -> 755ms [-0.32%, +1%]
  • ✔️as_biadjacency_matrix: 772ms -> 765ms [-2.33%, +0.47%]
  • ✔️as_data_frame_both: 1.61ms -> 1.62ms [-1.66%, +3.79%]
  • ✔️as_long_data_frame: 4.05ms -> 4.16ms [-0.58%, +5.62%]
  • ✔️es_attr_filter: 2.71ms -> 2.72ms [-1.34%, +1.93%]
  • ✔️graph_from_adjacency_matrix: 120ms -> 122ms [-0.57%, +2.9%]
  • ✔️graph_from_data_frame: 3.76ms -> 3.72ms [-2.39%, +0.52%]
  • ✔️vs_attr_filter: 1.55ms -> 1.56ms [-1.64%, +2.76%]
  • ✔️vs_by_name: 989µs -> 994µs [-1.86%, +2.88%]
    Further explanation regarding interpretation and methodology can be found in the documentation.

krlmlr commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of #2845, which attributes the deprecation by calling the function from the caller's environment rather than by inspecting it.


Generated by Claude Code

@krlmlr krlmlr closed this Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants