Skip to content

Commit

Permalink
Dynamically convert EventHandler to functools.partial
Browse files Browse the repository at this point in the history
Instead of converting the functions up front and assigning them to the
instance, unbox the function from the EventHandler when it is requested via
__getattribute__. This reduces the size of the per-instance pickle, because
event handler bodies do not need to be included.
  • Loading branch information
masenf committed Mar 25, 2024
1 parent a2f9224 commit 319ff17
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 28 deletions.
47 changes: 19 additions & 28 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,6 @@ def __init__(
parent_state=self,
_reflex_internal_init=True,
)
# Convert the event handlers to functions.
self._init_event_handlers()

# Create a fresh copy of the backend variables for this instance
self._backend_vars = copy.deepcopy(
Expand All @@ -346,32 +344,6 @@ def __init__(
}
)

def _init_event_handlers(self, state: BaseState | None = None):
"""Initialize event handlers.
Allow event handlers to be called directly on the instance. This is
called recursively for all parent states.
Args:
state: The state to initialize the event handlers on.
"""
if state is None:
state = self

# Convert the event handlers to functions.
for name, event_handler in state.event_handlers.items():
if event_handler.is_background:
fn = _no_chain_background_task(type(state), name, event_handler.fn)
else:
fn = functools.partial(event_handler.fn, self)
fn.__module__ = event_handler.fn.__module__ # type: ignore
fn.__qualname__ = event_handler.fn.__qualname__ # type: ignore
setattr(self, name, fn)

# Also allow direct calling of parent state event handlers
if state.parent_state is not None:
self._init_event_handlers(state.parent_state)

def __repr__(self) -> str:
"""Get the string representation of the state.
Expand Down Expand Up @@ -1083,12 +1055,31 @@ def __getattribute__(self, name: str) -> Any:
if parent_state is not None:
return getattr(parent_state, name)

# Allow event handlers to be called on the instance directly.
event_handlers = super().__getattribute__("event_handlers")
if name in event_handlers:
handler = event_handlers[name]
if handler.is_background:
fn = _no_chain_background_task(type(self), name, handler.fn)
else:
fn = functools.partial(handler.fn, self)
fn.__module__ = handler.fn.__module__ # type: ignore
fn.__qualname__ = handler.fn.__qualname__ # type: ignore
return fn

backend_vars = super().__getattribute__("_backend_vars")
if name in backend_vars:
value = backend_vars[name]
else:
value = super().__getattribute__(name)

if isinstance(value, EventHandler):
# The event handler is inherited from a parent, so let the parent convert
# it to a callable function.
parent_state = super().__getattribute__("parent_state")
if parent_state is not None:
return getattr(parent_state, name)

if isinstance(value, MutableProxy.__mutable_types__) and (
name in super().__getattribute__("base_vars") or name in backend_vars
):
Expand Down
8 changes: 8 additions & 0 deletions tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,10 @@ class SubState(MainState):
def set_v3(self, v: int):
self.set_v2(v)

class SubSubState(SubState):
def set_v4(self, v: int):
self.set_v(v)

ms = MainState()
ms.set_v2(1)
assert ms.v == 1
Expand All @@ -1133,6 +1137,10 @@ def set_v3(self, v: int):
ms.substates[SubState.get_name()].set_v3(2)
assert ms.v == 2

# ensure handler can be called from substate (referencing grandparent handler)
ms.get_substate(tuple(SubSubState.get_full_name().split("."))).set_v4(3)
assert ms.v == 3


def test_computed_var_cached():
"""Test that a ComputedVar doesn't recalculate when accessed."""
Expand Down

0 comments on commit 319ff17

Please sign in to comment.