Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6725.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Event handlers and computed vars inherited from a state mixin now preserve the source function's custom attributes and keyword-only defaults.
8 changes: 2 additions & 6 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions tests/units/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
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."""

Expand All @@ -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."""

Expand Down
Loading