Skip to content

fix: call a function passed as an argument from the caller's environment - #2845

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

fix: call a function passed as an argument from the caller's environment#2845
krlmlr wants to merge 5 commits into
mainfrom
claude/deprecated-callback-functions-87kfjp-call-site

Conversation

@krlmlr

@krlmlr krlmlr commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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

Supersedes #2844, which closed the same gap by inspecting the function object instead.

The problem

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

  • deprecate_soft() stays quiet entirely, because it does not report a package's own use of a deprecated function;
  • 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 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 fix

Nothing is inspected and nothing is replayed. The call moves, so that lifecycle's own attribution lands where it belongs.

igraph already does this in four places: bfs(), dfs(), arpack() and cluster_leading_eigen() evaluate their callback in the environment they were called from, through their rho and env arguments, and are attributed correctly because of it. call_user_callback() and as_user_callback() generalize that to the remaining function arguments, without an argument to pass in and thread through: the function runs from a closure enclosed in the environment igraph was called from, which is the one lifecycle consults via topenv().

plot(make_ring(5), layout = layout.circle)
#> Warning: `layout.circle()` was deprecated in igraph 2.1.0.
#> ℹ Please use `layout_in_circle()` instead.

Because there is no inspection, this covers any deprecation the function may signal, including one that depends on how it was called, one whose message is assembled at run time, and one in a function from another package entirely.

Applied to plotting parameters (which covers plot(), tkplot(), rglplot(), the layout graph attribute and function-valued igraph_options()), layout_nicely(), layout_components(), tk_reshape(), vertex shapes, attribute combinations, local_scan(), printer callbacks, and the search callbacks that do not already take an environment.

What this changes for a caller

  • A deprecation is reported when the function runs, not when it is passed. A callback that is never called is never reported, and a defunct function errors from inside the callback rather than at the argument — but it errors against the call that chose it, which is the point. For add_shape() the warning arrives at the first plot() that uses the shape, not at registration.
  • parent.frame() inside a callback is now the environment igraph was called from, for every callback rather than only deprecated ones. That is the mechanism; it is a visible change for a callback that inspects its caller.
  • One extra frame per callback call.

Testing

New tests/testthat/test-utils-user-callbacks.R, with the assertions on message output as snapshots.

Getting these to mean anything took some care: tests 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. The first snapshot records both halves of the change in one place:

Code
  as_user(igraph_function(FALSE), deprecated_igraph_function("soft_plain()"))
  as_user(igraph_function(FALSE), deprecated_igraph_function("warn_plain()",
    "deprecate_warn"))
Condition
  Warning:
  `warn_plain()` was deprecated in igraph 2.0.0.
  i The deprecated feature was likely used in the igraph package.
    Please report the issue at <https://github.com/igraph/rigraph/issues>.
Code
  as_user(igraph_function(TRUE), deprecated_igraph_function("soft_wrapped()"))
Condition
  Warning:
  `soft_wrapped()` was deprecated in igraph 2.0.0.
Code
  as_user(igraph_function(TRUE), deprecated_igraph_function("warn_wrapped()",
    "deprecate_warn"))
Condition
  Warning:
  `warn_wrapped()` was deprecated in igraph 2.0.0.

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).

Related

Signalling a deprecation from inside a callback that igraph invokes from C used to crash, because lifecycle reaches vcount() through the backtrace it assembles and the nested call unwound the running algorithm's structures. #2851 fixes that; it is independent of this PR, but the two meet whenever a deprecated function is used as a search callback.

  • 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 added 2 commits August 16, 2026 13:37
igraph functions that take another function as an argument call it themselves.
lifecycle attributes a deprecation to the caller of the deprecated function,
and that caller is an igraph frame:
`deprecate_soft()` says nothing at all,
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 chose is on its way out.

`bfs()`, `dfs()`, `arpack()` and `cluster_leading_eigen()` already evaluate
their callback in the environment they were called from, through their `rho`
and `env` arguments, and are attributed correctly because of it.
`call_user_callback()` and `as_user_callback()` extend that to the remaining
function arguments, without an argument to pass in and thread through:
the function is called from a closure enclosed in the environment igraph was
called from, which is the one lifecycle asks about.

Nothing is inspected and nothing is replayed, so this covers any deprecation
the function may signal, including one that depends on how it was called.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJx1uH2GvN5DcqdVzDLvwW
`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, and their hard deprecation -- made after 2.3.3 and
never released -- would break `plot(layout = )` for users who have never seen
the warning, because igraph, not they, called the function.
Their documentation already describes this behaviour.

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 7e6a3be is merged into main:

  • ✔️as_adjacency_matrix: 724ms -> 725ms [-0.3%, +0.4%]
  • ✔️as_biadjacency_matrix: 728ms -> 730ms [-0.16%, +0.67%]
  • ✔️as_data_frame_both: 1.56ms -> 1.56ms [-0.97%, +1.16%]
  • ✔️as_long_data_frame: 3.94ms -> 3.93ms [-1.03%, +0.41%]
  • ✔️es_attr_filter: 2.64ms -> 2.63ms [-1.32%, +0.69%]
  • ✔️graph_from_adjacency_matrix: 116ms -> 117ms [-0.37%, +1.76%]
  • ✔️graph_from_data_frame: 3.64ms -> 3.63ms [-0.85%, +0.8%]
  • ✔️vs_attr_filter: 1.49ms -> 1.48ms [-1.69%, +0.5%]
  • ✔️vs_by_name: 963µs -> 941µs [-6.01%, +1.44%]
    Further explanation regarding interpretation and methodology can be found in the documentation.

The hard deprecation of `layout.spring()`, `layout.svd()` and
`layout.fruchterman.reingold.grid()` stands:
a defunct layout function passed to `plot()` now fails against the call that
chose it, which is what un-deprecating them was for.
This reverts commit 7e6a3be and the
documentation the roxygen run derived from it.

The tests that assert on message output are snapshot tests now.
One of them records what a deprecation looked like before and after in the same
place, which says more than a regular expression can.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJx1uH2GvN5DcqdVzDLvwW
@krlmlr
krlmlr marked this pull request as ready for review August 16, 2026 14:13
@github-actions

Copy link
Copy Markdown
Contributor

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

  • 🚀as_adjacency_matrix: 816ms -> 804ms [-2.76%, -0.02%]
  • ✔️as_biadjacency_matrix: 819ms -> 818ms [-1.85%, +1.58%]
  • ✔️as_data_frame_both: 1.75ms -> 1.74ms [-2.28%, +1.19%]
  • ✔️as_long_data_frame: 4.2ms -> 4.27ms [-0.73%, +4.01%]
  • ✔️es_attr_filter: 2.91ms -> 2.89ms [-3.6%, +2.32%]
  • ✔️graph_from_adjacency_matrix: 149ms -> 147ms [-2.57%, +0.82%]
  • ✔️graph_from_data_frame: 3.92ms -> 3.88ms [-2.4%, +0.74%]
  • ✔️vs_attr_filter: 1.67ms -> 1.68ms [-2.87%, +3.63%]
  • ✔️vs_by_name: 1.11ms -> 1.11ms [-2.58%, +1.59%]
    Further explanation regarding interpretation and methodology can be found in the documentation.

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