From 9453a71e5448d7a17e3dff8fba18737020e0c168 Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 9 Jul 2026 14:01:28 +0500 Subject: [PATCH 1/4] fix(state): preserve source function __dict__ and kwdefaults in _copy_fn (ENG-9859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the State metaclass pulls a computed-var getter or event handler in from a mixin, _copy_fn rebuilds the function via FunctionType, which drops the function's __dict__. Previously it hand-copied only the background-task and event-actions markers back on, silently losing any other attribute a framework or downstream user set on the getter/handler. Copy the full __dict__ onto the copy, which subsumes the per-marker copies and lets arbitrary markers (e.g. Reflex Enterprise's _auth tag) survive mixin inheritance. Also carry __kwdefaults__, which FunctionType likewise drops — without it a mixin handler with a keyword-only default raised TypeError when called through the subclass. --- reflex/state.py | 8 +--- tests/units/test_state.py | 82 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 3762ad0ac2a..a488cab97e7 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -31,7 +31,6 @@ from reflex_base.constants.state import FIELD_MARKER from reflex_base.environment import PerformanceMode, environment from reflex_base.event import ( - BACKGROUND_TASK_MARKER, EVENT_ACTIONS_MARKER, Event, EventHandler, @@ -755,11 +754,8 @@ def _copy_fn(fn: Callable) -> Callable: closure=fn.__closure__, ) newfn.__annotations__ = fn.__annotations__ - if mark := getattr(fn, BACKGROUND_TASK_MARKER, None): - setattr(newfn, BACKGROUND_TASK_MARKER, mark) - # Preserve event_actions from @rx.event decorator - if event_actions := getattr(fn, EVENT_ACTIONS_MARKER, None): - object.__setattr__(newfn, EVENT_ACTIONS_MARKER, event_actions) + newfn.__kwdefaults__ = fn.__kwdefaults__ + newfn.__dict__.update(fn.__dict__) return newfn @staticmethod diff --git a/tests/units/test_state.py b/tests/units/test_state.py index b0aaf6e022f..49a22419ec0 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -4243,6 +4243,73 @@ def test_bare_mixin_state() -> None: assert ChildBareMixinState.get_root_state() == State +class MarkerMixin(State, mixin=True): + """A mixin state with a getter and handler carrying custom function attributes.""" + + @rx.var + def marked_computed(self) -> str: + """A computed var whose getter is tagged with a custom attribute. + + Returns: + A static string. + """ + return "marked" + + marked_computed.fget._custom_marker = object() # pyright: ignore [reportFunctionMemberAccess] + + def marked_handler(self): + """An event handler tagged with a custom attribute.""" + + marked_handler._custom_marker = object() # pyright: ignore [reportFunctionMemberAccess] + + def kwonly_default_handler(self, *, count: int = 1) -> int: + """An event handler with a keyword-only default argument. + + Args: + count: A keyword-only argument with a default. + + Returns: + The count. + """ + return count + + +class UsesMarkerMixin(MarkerMixin, State): + """A state that pulls in the marked mixin getter/handler.""" + + +def test_copy_fn_preserves_custom_function_attributes() -> None: + """Test that _copy_fn preserves arbitrary attributes set on mixin functions.""" + orig_computed_fget = MarkerMixin.__dict__["marked_computed"].fget + copied_computed_fget = UsesMarkerMixin.computed_vars["marked_computed"].fget + assert copied_computed_fget is not orig_computed_fget + assert ( + copied_computed_fget._custom_marker # pyright: ignore [reportFunctionMemberAccess] + is orig_computed_fget._custom_marker # pyright: ignore [reportFunctionMemberAccess] + ) + + orig_handler_fn = MarkerMixin.__dict__["marked_handler"] + copied_handler_fn = UsesMarkerMixin.event_handlers["marked_handler"].fn + assert copied_handler_fn is not orig_handler_fn + assert ( + copied_handler_fn._custom_marker # pyright: ignore [reportFunctionMemberAccess] + is orig_handler_fn._custom_marker # pyright: ignore [reportFunctionMemberAccess] + ) + + # The copy's __dict__ is independent of the source function's __dict__. + assert copied_handler_fn.__dict__ is not orig_handler_fn.__dict__ + copied_handler_fn.__dict__["_leaked"] = True + assert "_leaked" not in orig_handler_fn.__dict__ + + +def test_copy_fn_preserves_kwonly_defaults() -> None: + """Test that _copy_fn preserves keyword-only default arguments.""" + handler_fn = UsesMarkerMixin.event_handlers["kwonly_default_handler"].fn + assert handler_fn.__kwdefaults__ == {"count": 1} + instance = UsesMarkerMixin(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] + assert handler_fn(instance) == 1 + + def test_mixin_event_handler_preserves_event_actions() -> None: """Test that event_actions from @rx.event decorator are preserved when inherited from mixins.""" @@ -4258,6 +4325,21 @@ class UsesEventActionsMixin(EventActionsMixin, State): assert handler.event_actions == {"preventDefault": True, "stopPropagation": True} +def test_mixin_event_handler_preserves_background_task_marker() -> None: + """Test that the background task marker is preserved when inherited from mixins.""" + + class BackgroundTaskMixin(BaseState, mixin=True): + @rx.event(background=True) + async def handle_in_background(self): + pass + + class UsesBackgroundTaskMixin(BackgroundTaskMixin, State): + pass + + handler = UsesBackgroundTaskMixin.handle_in_background + assert handler.is_background # pyright: ignore [reportAttributeAccessIssue] + + def test_assignment_to_undeclared_vars(): """Test that an attribute error is thrown when undeclared vars are set.""" From 40d8d0f637d0ea7c5b4ef625936159af872aa668 Mon Sep 17 00:00:00 2001 From: Farhan Date: Fri, 10 Jul 2026 20:51:14 +0500 Subject: [PATCH 2/4] chore: add changelog entry for #6725 --- news/6725.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6725.bugfix.md diff --git a/news/6725.bugfix.md b/news/6725.bugfix.md new file mode 100644 index 00000000000..0a16562f77f --- /dev/null +++ b/news/6725.bugfix.md @@ -0,0 +1 @@ +Event handlers and computed vars copied from a state mixin now preserve all attributes of the source function's `__dict__` (not just the background-task and event-action markers), so custom decorator attributes survive mixin inheritance. Keyword-only default arguments (`__kwdefaults__`) are also preserved, fixing a `TypeError` when calling a copied handler that relied on them. From 187504c2ab1679af5a76b7ad75af6a7744d865c7 Mon Sep 17 00:00:00 2001 From: Farhan Date: Fri, 10 Jul 2026 20:52:25 +0500 Subject: [PATCH 3/4] test: drop unneeded _reflex_internal_init in pytest case --- tests/units/test_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 49a22419ec0..0aea285babd 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -4306,7 +4306,7 @@ def test_copy_fn_preserves_kwonly_defaults() -> None: """Test that _copy_fn preserves keyword-only default arguments.""" handler_fn = UsesMarkerMixin.event_handlers["kwonly_default_handler"].fn assert handler_fn.__kwdefaults__ == {"count": 1} - instance = UsesMarkerMixin(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] + instance = UsesMarkerMixin() assert handler_fn(instance) == 1 From 6fc747c796a31c45c4e98155b8fdb684025ccaea Mon Sep 17 00:00:00 2001 From: Farhan Date: Fri, 10 Jul 2026 20:53:03 +0500 Subject: [PATCH 4/4] chore: shorten changelog entry --- news/6725.bugfix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/6725.bugfix.md b/news/6725.bugfix.md index 0a16562f77f..68441f4ae38 100644 --- a/news/6725.bugfix.md +++ b/news/6725.bugfix.md @@ -1 +1 @@ -Event handlers and computed vars copied from a state mixin now preserve all attributes of the source function's `__dict__` (not just the background-task and event-action markers), so custom decorator attributes survive mixin inheritance. Keyword-only default arguments (`__kwdefaults__`) are also preserved, fixing a `TypeError` when calling a copied handler that relied on them. +Event handlers and computed vars inherited from a state mixin now preserve the source function's custom attributes and keyword-only defaults.