Merge a repeated @rx.event; restore staticmethod/classmethod above @rx.event
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 informat.py:448with'EventHandler' object has no attribute '__qualname__'; - the background check failed with
Background task must be async function or generator.; setattron the frozen dataclass raisedFrozenInstanceError.
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__.pywrapper: when the input is anEventHandler, unwrap it tofunc.fnand decorate the function again, silently (owner's decision: the deprecation covers non-rx.eventdecorators). Merge semantics:background/supersedesmarkers live on the function and take the explicit value of each decorator, so an outerbackground=Falseturns off an innerbackground=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 aTypeError, as on main. Unwrapping it would write the new options onto the other state's function:background=Truewould makeOtherState.handlera background task.
reflex/state.py: a new_method_above_handlerturnsstaticmethod(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 classmethoddeprecation (the decorator has no effect; remove it), as the #7673 changelog says for@rx.eventbelow them.
Test plan
Regression tests written first and shown failing on origin/main (2d71015b7):
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. Alsotest_repeated_rx_event_merges_event_actionsandtest_repeated_rx_event_on_a_decentralized_handler.test_repeated_rx_event_outer_false_overrides_background_and_supersedesandtest_rx_event_on_a_handler_of_another_state_raises[*]. Before their fix:assert True is False(outerbackground=Falseignored) and 3×DID NOT RAISE TypeError(OtherState.handledecorated again, and made a background task withbackground=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 anEventSpec.
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.pywas excluded because the sandbox has no Chromium for it.ruff check,ruff formatanduv run pyright reflex testsare clean (0 errors).- reflex-examples
overkey(copied from @ebe19ff), with a throwaway venv that has this branch installed editable:import overkey.overkeyworks andOverkeyState.tick.is_backgroundis True.reflex runin dev serves the page, and in Chromium a key press starts the stacked backgroundtick: the timer goes 29 → 26 in 2.5 s, with no console or backend errors.
Notes
- Not changed:
@staticmethodabove@rx.eventon a class that is not a state still returns anEventSpec, with no warning. Nothing inspects such a class's attributes (staticmethoddoes not forward__set_name__). The #7673 changelog already says calling such a method through the class is unsupported. A follow-up could warn inEventHandler.__call__for an unbound method-like handler. - pyright still rejects
@rx.event(...)stacked above another@rx.event(theEventDecoratorsignature 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.eventon Python 3.11–3.13) is handled in a separate PR.
Generated by Claude Code