Skip to content

Merge a repeated @rx.event; restore staticmethod/classmethod above @rx.event - #7807

Merged
masenf merged 2 commits into
mainfrom
fix/repeated-rx-event
Nov 11, 2026
Merged

masenf merged 2 commits into
mainfrom
fix/repeated-rx-event

Conversation

@masenf

@masenf masenf commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

This is a release fix-up for FINDING-001 (HIGH, regression) and FINDING-016 (LOW, regression) from the reflex 0.11.0a1 pre-release testing (artifacts on branch claude/vibrant-pasteur-3921g3, prerelease-testing/runs/2026-11-11-v0.11.0a1/).

Defect

FINDING-001. A second @rx.event on a handler makes class creation fail. reflex-examples overkey (overkey/overkey.py:164, @rx.event directly above @rx.event(background=True)) fails at import, and reflex run exits 1. Since #7673, @rx.event returns an EventHandler, and EventNamespace.__new__.wrapper had no case for an input that is already one. Depending on the outer decorator's options, one of three things happened:

  • it built EventHandler(fn=EventHandler), and registration failed in format.py:448 with 'EventHandler' object has no attribute '__qualname__';
  • the background check failed with Background task must be async function or generator.;
  • setattr on the frozen dataclass raised FrozenInstanceError.

All six stacking patterns failed: event/event, event/event(background=True), event/event(throttle=300), event(background=True)/event, event(throttle=300)/event and event()/event.

FINDING-016. @staticmethod (or @classmethod) stacked above @rx.event on a state returned an EventSpec instead of calling the function. BaseState._item_is_event_handler found the inner handler through staticmethod.__wrapped__ and emitted the generic, misleading "Decorators above @rx.event" warning. A staticmethod has no __code__, so the attribute stayed staticmethod(EventHandler), and calls went to EventHandler.__call__.

Fix

  • reflex_base/event/__init__.py wrapper: when the input is an EventHandler, unwrap it to func.fn and decorate the function again, silently (owner's decision: the deprecation covers non-rx.event decorators). Merge semantics:
    • background / supersedes markers live on the function and take the explicit value of each decorator, so an outer background=False turns off an inner background=True. The outer decorator wins for every option, the same rule as for event actions below.
    • Event actions are {**inner, **outer}: the outer decorator wins where both set an action, because it is applied last, the same way chained .throttle(...) overrides. In 0.10.1 the outer decorator replaced the whole dict whenever it had actions. The merge gives the same result for every pattern above and drops nothing.
    • A repeated decorator on a decentralized (module-level) handler re-registers it under the name it already has, instead of registering a second, doubly-prefixed handler.
    • Only an unbound handler (the inner decorator of a stack) or a decentralized one is decorated again. A method handler already bound to a state, such as rx.event(OtherState.handler) in another state, raises a TypeError, as on main. Unwrapping it would write the new options onto the other state's function: background=True would make OtherState.handler a background task.
  • reflex/state.py: a new _method_above_handler turns staticmethod(EventHandler) / classmethod(EventHandler) back into the plain staticmethod/classmethod of its function. It is used in the handler loop of __init_subclass__ and in _bind_mixin_members, so a state that copies the method from a mixin is fixed too. It warns with the existing @rx.event on a staticmethod or classmethod deprecation (the decorator has no effect; remove it), as the Make event handlers awaitable and register them through @rx.event #7673 changelog says for @rx.event below them.

Test plan

Regression tests written first and shown failing on origin/main (2d71015):

  • tests/units/test_event.py::test_repeated_rx_event_is_one_handler[*]: the six patterns each register one handler with the right name, background flag and actions, emit no deprecation, and run. Also test_repeated_rx_event_merges_event_actions and test_repeated_rx_event_on_a_decentralized_handler.
  • test_repeated_rx_event_outer_false_overrides_background_and_supersedes and test_rx_event_on_a_handler_of_another_state_raises[*]. Before their fix: assert True is False (outer background=False ignored) and 3× DID NOT RAISE TypeError (OtherState.handle decorated again, and made a background task with background=True).
    Before the fix, all 8 failed: 4× AttributeError: 'EventHandler' object has no attribute '__qualname__' (format.py:448), 1× TypeError: Background task must be async function or generator., 3× FrozenInstanceError: cannot assign to field '_rx_event_actions'.
  • tests/units/test_state.py::test_static_and_class_methods_above_rx_event_are_deprecated_and_ignored: S.helper(3) == 6, S().helper(3) == 6, S.cls_helper(3) runs, nothing is registered as a handler, and the same holds via a mixin. Before the fix it failed: assert {'Decorators above @rx.event'} == {'@rx.event on a staticmethod or classmethod'}, and the call returned an EventSpec.

After the fix:

  • uv run pytest tests/units --cov --no-cov-on-fail --cov-report=: 15643 passed, 66 skipped, coverage 81.43%. tests/units/reflex_bench/drivers/test_browser.py was excluded because the sandbox has no Chromium for it.
  • ruff check, ruff format and uv run pyright reflex tests are clean (0 errors).
  • reflex-examples overkey (copied from @ebe19ff), with a throwaway venv that has this branch installed editable: import overkey.overkey works and OverkeyState.tick.is_background is True. reflex run in dev serves the page, and in Chromium a key press starts the stacked background tick: the timer goes 29 → 26 in 2.5 s, with no console or backend errors.

Notes

  • Not changed: @staticmethod above @rx.event on a class that is not a state still returns an EventSpec, with no warning. Nothing inspects such a class's attributes (staticmethod does not forward __set_name__). The Make event handlers awaitable and register them through @rx.event #7673 changelog already says calling such a method through the class is unsupported. A follow-up could warn in EventHandler.__call__ for an unbound method-like handler.
  • pyright still rejects @rx.event(...) stacked above another @rx.event (the EventDecorator signature takes a plain function). The tests carry # pyright: ignore. Typing the repeated form is left out of this fix-up.
  • FINDING-007 (pyright typing of bare @rx.event on Python 3.11–3.13) is handled in a separate PR.

Generated by Claude Code

View guided diff Turn on auto-fix

@masenf
masenf requested a review from a team as a code owner November 11, 2026 08:56
@masenf masenf added this to the v0.11.0 milestone Nov 11, 2026
@masenf masenf added the on deck PRs lined up to review / merge next label Nov 11, 2026
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] This PR appears safe to merge; no new actionable issues were found.

Summary

This PR lets an outer @rx.event explicitly turn off background and supersedes. It also rejects redecorating methods already bound to a state.

  • Stacked @rx.event decorators now build one handler with the outer settings.

Reviews (6) · Last reviewed commit: "Say which handlers a repeated rx.event r..." · Reviewed by Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread tests/units/test_state.py Resolved
Comment thread tests/units/test_event.py Resolved
@codspeed

codspeed Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 2.33%

2 warnings

⚠️ 16 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 3 benchmarks with a significant change ran on different hardware

Their base and head results were measured on machines with a different CPU, core count or memory, so the changes might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 148 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_state_collection_assignment[100000] 1 s 1.1 s -5.89%
❌ test_route_matching[1000] 48.1 µs 50.8 µs -5.33%
⚡ test_process_event_burst_independent_tokens[1] 2.9 ms 2.7 ms +4.6%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/repeated-rx-event (84d0a23) with main (3184cad)

Open in CodSpeed

@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

unit-tests (windows-latest, 3.15) failed in tests/units/reflex_base/utils/test_supervisor.py::test_supervise_output_does_not_wait_for_lingering_descendants (assert 30.139 < 10). This PR doesn't cause it: the diff doesn't touch reflex_base/utils/supervisor.py (from #7650), and the same module also stalled #7803's Windows 3.14 leg. Every other unit-test leg here passed. It is an intermittent Windows bug in the supervisor, filed as #7819 with a repro and the constraints for a fix. No fix exists yet. The workflow is still running other jobs, so the failed job can't be re-run yet; I'll re-run it once when the run finishes.


Generated by Claude Code

@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

CodSpeed is red here (test_route_matching[10] -5.6%, test_process_event_burst_independent_tokens[1] -4.8%), but this PR doesn't cause it. CodSpeed flags both as measured on different hardware, and both show the same 4–6% drop on every PR in this batch. This PR's runtime changes run only when a handler is decorated or a state class is created, not in route matching or event processing (see the local state-access measurements on #7810). CodSpeed is not a required check; leaving it for acknowledgement on CodSpeed.


Generated by Claude Code

masenf pushed a commit that referenced this pull request Nov 11, 2026
…runs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
FarhanAliRaza and others added 2 commits November 11, 2026 18:36
…persedes

`rx.event(A.h, ...)` in another state unwrapped A's handler and wrote the
new options onto A's function, so `background=True` turned A.h into a
background task. It now raises a TypeError, as before the repeated
decorator was merged. Only an unbound handler (the inner decorator of a
stack) or a decentralized one is decorated again.

An outer `background=False` or `supersedes=False` left the inner True in
place, while an outer False event action overrides the inner one. Both
markers now take the explicit value, so the outer decorator wins for
every option.
@masenf
masenf force-pushed the fix/repeated-rx-event branch from d318f18 to 84d0a23 Compare November 11, 2026 18:37
@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

#7810 was stacked on this PR, so its squash (b95fbf5) put this PR's first two commits on main (65e0d02, which merges a repeated @rx.event and restores staticmethod/classmethod above it, and d502726, the outer False/0 and __main__ handling). It did not include the two later commits, so I rebased them onto main. Rewriting the branch kept each commit's author, and --force-with-lease was pinned to the previous head d318f18. This PR now holds only:

  • 47fdf2f (FarhanAliRaza): reject a handler already bound to another state, and let an outer False undo background / supersedes.
  • 84d0a23: the news entry now says which handlers a repeated rx.event rejects.

The diff against main is reflex_base/event/__init__.py, tests/units/test_event.py and the news fragment (+56/−10). Locally, test_event.py and test_state.py pass (480 tests), and ruff and pyright (including tests/type_checking) are clean.


Generated by Claude Code

masenf pushed a commit that referenced this pull request Nov 11, 2026
…maining commits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

CodSpeed on 84d0a23 flags test_state_collection_assignment[100000] (−5.9%) and test_route_matching[1000] (−5.3%). Neither comes from this PR. The source change is in rx.event decoration (reflex_base/event/__init__.py), which runs once per handler at class creation. It doesn't run when a state field is assigned or a route is matched. Both benchmarks drift across unrelated PRs today: test_route_matching swings ±6% between runs, and test_state_collection_assignment swings at every size. CodSpeed also reports three benchmarks in this run measured on different hardware.


Generated by Claude Code

@masenf masenf left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@masenf
masenf merged commit 7e46fc1 into main Nov 11, 2026
167 of 168 checks passed
@masenf
masenf deleted the fix/repeated-rx-event branch November 11, 2026 19:03
masenf added a commit that referenced this pull request Nov 11, 2026
…x.event (#7807)

* Reject another state's handler; let an outer False undo background/supersedes

`rx.event(A.h, ...)` in another state unwrapped A's handler and wrote the
new options onto A's function, so `background=True` turned A.h into a
background task. It now raises a TypeError, as before the repeated
decorator was merged. Only an unbound handler (the inner decorator of a
stack) or a decentralized one is decorated again.

An outer `background=False` or `supersedes=False` left the inner True in
place, while an outer False event action overrides the inner one. Both
markers now take the explicit value, so the outer decorator wins for
every option.

* Say which handlers a repeated rx.event rejects in the news entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi

---------

Co-authored-by: Farhan <www.mfarvirus@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit 7e46fc1)
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf masenf added the release-train Issues found during agentic /prerelease-testing fanout, not necessarily validated bugs. label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on deck PRs lined up to review / merge next release-train Issues found during agentic /prerelease-testing fanout, not necessarily validated bugs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants