From 507042a0dae34c5e87ae0094bd33e6eb697710d9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 00:51:45 +0000 Subject: [PATCH 01/11] Implement decentralized event handlers Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 110 +++++++++++++++++- reflex/state.py | 22 +++- reflex/utils/format.py | 5 + .../test_decentralized_event_handlers.py | 110 ++++++++++++++++++ 4 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 tests/integration/test_decentralized_event_handlers.py diff --git a/reflex/event.py b/reflex/event.py index afb03367cb5..60ae5fa4c48 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -55,6 +55,30 @@ ) from reflex.vars.object import ObjectVar +_global_event_handlers: dict[str, EventHandler] = {} + + +def register_event_handler(name: str, handler: EventHandler) -> None: + """Register a decentralized event handler. + + Args: + name: The name of the event handler. + handler: The event handler. + """ + _global_event_handlers[name] = handler + + +def get_event_handler(name: str) -> EventHandler | None: + """Get a decentralized event handler by name. + + Args: + name: The name of the event handler. + + Returns: + The event handler, or None if not found. + """ + return _global_event_handlers.get(name) + @dataclasses.dataclass( init=True, @@ -178,7 +202,7 @@ class EventHandler(EventActionsMixin): # The full name of the state class this event handler is attached to. # Empty string means this event handler is a server side event. - state_full_name: str = dataclasses.field(default="") + state_full_name: str | None = dataclasses.field(default="") @classmethod def __class_getitem__(cls, args_spec: str) -> Annotated: @@ -261,6 +285,13 @@ def __call__(self, *args: Any, **kwargs: Any) -> EventSpec: ) from e payload = tuple(zip(fn_args, values, strict=False)) + # Check if this is a decentralized event handler + if self.state_full_name is None: + from reflex.utils import format + + name = format.to_snake_case(self.fn.__qualname__) + register_event_handler(name, self) + # Return the event spec. return EventSpec( handler=self, args=payload, event_actions=self.event_actions.copy() @@ -1492,6 +1523,9 @@ def check_fn_match_arg_spec( Raises: EventFnArgMismatchError: Raised if the number of mandatory arguments do not match """ + if is_decentralized_event_handler(user_func): + return + user_args = list(inspect.signature(user_func).parameters) # Drop the first argument if it's a bound method if inspect.ismethod(user_func) and user_func.__self__ is not None: @@ -1518,6 +1552,61 @@ def check_fn_match_arg_spec( ) +DECENTRALIZED_EVENT_MARKER = "_rx_decentralized_event" + + +def is_decentralized_event_handler(fn: Callable) -> bool: + """Check if a function is a decentralized event handler. + + Args: + fn: The function to check. + + Returns: + Whether the function is a decentralized event handler. + """ + # Check if the function has been decorated with @rx.event + if not hasattr(fn, "__qualname__"): + return False + + # Check if the function has the decentralized event marker + return hasattr(fn, DECENTRALIZED_EVENT_MARKER) + + +def wrap_decentralized_handler(fn: Callable) -> Callable: + """Wrap a decentralized event handler to be used with component events. + + This creates a wrapper that doesn't require the state parameter when called + from a component event, but will pass the state when the event is processed. + + Args: + fn: The decentralized event handler to wrap. + + Returns: + A wrapped function that can be used with component events. + """ + + # Create a wrapper function that doesn't require the state parameter + def wrapper(*args, **kwargs): + # Get or create the event handler + from reflex.utils import format + + name = format.to_snake_case(fn.__qualname__) + handler = _global_event_handlers.get(name) + if handler is None: + handler = EventHandler(fn=fn, state_full_name=None) + register_event_handler(name, handler) + + # Create an event spec with no arguments - the state will be provided + return EventSpec(handler=handler, args=()) + + wrapper.__name__ = fn.__name__ + wrapper.__qualname__ = fn.__qualname__ + wrapper.__doc__ = fn.__doc__ + wrapper.__module__ = fn.__module__ + + return wrapper + + def call_event_fn( fn: Callable, arg_spec: ArgsSpec | Sequence[ArgsSpec], @@ -1543,6 +1632,11 @@ def call_event_fn( from reflex.event import EventHandler, EventSpec from reflex.utils.exceptions import EventHandlerValueError + # Check if this is a decentralized event handler + if is_decentralized_event_handler(fn): + wrapped_fn = wrap_decentralized_handler(fn) + return call_event_fn(wrapped_fn, arg_spec, key=key) + # Check that fn signature matches arg_spec check_fn_match_arg_spec(fn, arg_spec, key=key) @@ -2066,7 +2160,19 @@ def wrapper( setattr(func, BACKGROUND_TASK_MARKER, True) if getattr(func, "__name__", "").startswith("_"): raise ValueError("Event handlers cannot be private.") - return func # pyright: ignore [reportReturnType] + + # Check if this is a method (defined in a class) or a standalone function + if hasattr(func, "__qualname__") and "." in func.__qualname__: + return func # pyright: ignore [reportReturnType] + else: + # This is a decentralized event handler + handler = EventHandler(fn=func, state_full_name=None) + if background: + setattr(handler, BACKGROUND_TASK_MARKER, True) + # Mark the function as a decentralized event handler + setattr(func, DECENTRALIZED_EVENT_MARKER, True) + # Return the original function so it can be called normally + return func # pyright: ignore [reportReturnType] if func is not None: return wrapper(func) diff --git a/reflex/state.py b/reflex/state.py index 588383518b9..961a39bab8d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1586,15 +1586,30 @@ def _get_event_handler( Args: event: The event to get the handler for. - Returns: The event handler. Raises: ValueError: If the event handler or substate is not found. + EventHandlerValueError: If the event handler is not found. """ # Get the event handler. path = event.name.split(".") + + if "." not in event.name: + from reflex.event import get_event_handler + from reflex.utils.exceptions import EventHandlerValueError + + handler = get_event_handler(event.name) + if handler is None: + raise EventHandlerValueError(f"Event handler {event.name} not found.") + + # For background tasks, proxy the state + if handler.is_background: + return StateProxy(self), handler + + return self, handler + path, name = path[:-1], path[-1] substate = self.get_substate(path) if not substate: @@ -1753,7 +1768,10 @@ async def _process_event( from reflex.utils import telemetry # Get the function to process the event. - fn = functools.partial(handler.fn, state) + if handler.state_full_name is None: + fn = handler.fn + else: + fn = functools.partial(handler.fn, state) try: type_hints = typing.get_type_hints(handler.fn) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index cba91b4ddb9..12619e90115 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -483,6 +483,11 @@ def format_event_handler(handler: EventHandler) -> str: Returns: The formatted function. """ + if handler.state_full_name is None: + from reflex.utils import format + + return format.to_snake_case(handler.fn.__qualname__) + state, name = get_event_handler_parts(handler) if state == "": return name diff --git a/tests/integration/test_decentralized_event_handlers.py b/tests/integration/test_decentralized_event_handlers.py new file mode 100644 index 00000000000..87aed635caa --- /dev/null +++ b/tests/integration/test_decentralized_event_handlers.py @@ -0,0 +1,110 @@ +"""Test decentralized event handlers functionality.""" + +import pytest +from selenium.webdriver.common.by import By + +from reflex.testing import AppHarness, WebDriver + + +def DecentralizedEventHandlers(): + """Test that decentralized event handlers work as expected.""" + import reflex as rx + + class TestState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + """Increment the counter.""" + self.count += 1 + + @rx.event + def on_load(state: TestState): + """Event handler for loading the state. + + Args: + state: The state to modify. + """ + state.count = 10 + + @rx.event + def reset_count(state: TestState): + """Event handler for resetting the count. + + Args: + state: The state to modify. + """ + state.count = 0 + + def index(): + return rx.vstack( + rx.heading(TestState.count, id="counter"), + rx.button("Increment", on_click=TestState.increment, id="increment"), + rx.button("Reset", on_click=reset_count, id="reset"), + rx.text("Loaded", on_mount=on_load, id="loaded"), + ) + + app = rx.App() + app.add_page(index) + + +@pytest.fixture(scope="module") +def decentralized_handlers( + tmp_path_factory, +): + """Start DecentralizedEventHandlers app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("decentralized_handlers"), + app_source=DecentralizedEventHandlers, + ) as harness: + yield harness + + +@pytest.fixture +def driver(decentralized_handlers: AppHarness): + """Get an instance of the browser open to the app. + + Args: + decentralized_handlers: harness for DecentralizedEventHandlers app + + Yields: + WebDriver instance. + """ + assert decentralized_handlers.app_instance is not None, "app is not running" + driver = decentralized_handlers.frontend() + try: + yield driver + finally: + driver.quit() + + +def test_decentralized_event_handlers( + decentralized_handlers: AppHarness, + driver: WebDriver, +): + """Test that decentralized event handlers work as expected. + + Args: + decentralized_handlers: harness for DecentralizedEventHandlers app + driver: WebDriver instance + """ + assert decentralized_handlers.app_instance is not None + + counter = driver.find_element(By.ID, "counter") + increment_button = driver.find_element(By.ID, "increment") + reset_button = driver.find_element(By.ID, "reset") + + assert decentralized_handlers._poll_for(lambda: counter.text == "10", timeout=5) + + increment_button.click() + assert decentralized_handlers._poll_for(lambda: counter.text == "11", timeout=5) + + reset_button.click() + assert decentralized_handlers._poll_for(lambda: counter.text == "0", timeout=5) From 9601b0d7d2b19120ea4754eeb3947b385ff76a05 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:06:11 +0000 Subject: [PATCH 02/11] Fix decentralized event handlers with parameters Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 23 ++++++++++++++++--- reflex/state.py | 10 ++++++++ .../test_decentralized_event_handlers.py | 19 +++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 60ae5fa4c48..9075fd7fa89 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -1596,8 +1596,24 @@ def wrapper(*args, **kwargs): handler = EventHandler(fn=fn, state_full_name=None) register_event_handler(name, handler) - # Create an event spec with no arguments - the state will be provided - return EventSpec(handler=handler, args=()) + # Create an event spec with the provided arguments + arg_specs = [] + + for i, arg in enumerate(args): + # Create a var for the arg + var_arg = Var.create(arg) + + # Add the arg to the arg specs + arg_specs.append((Var.create_safe(f"arg{i}"), var_arg)) + + for name, arg in kwargs.items(): + # Create a var for the arg + var_arg = Var.create(arg) + + # Add the arg to the arg specs + arg_specs.append((Var.create_safe(name), var_arg)) + + return EventSpec(handler=handler, args=tuple(arg_specs)) wrapper.__name__ = fn.__name__ wrapper.__qualname__ = fn.__qualname__ @@ -1635,7 +1651,8 @@ def call_event_fn( # Check if this is a decentralized event handler if is_decentralized_event_handler(fn): wrapped_fn = wrap_decentralized_handler(fn) - return call_event_fn(wrapped_fn, arg_spec, key=key) + # Call the wrapped function directly without passing arg_spec + return [wrapped_fn()] # Check that fn signature matches arg_spec check_fn_match_arg_spec(fn, arg_spec, key=key) diff --git a/reflex/state.py b/reflex/state.py index 961a39bab8d..040e9e26453 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1597,6 +1597,16 @@ def _get_event_handler( path = event.name.split(".") if "." not in event.name: + cls = type(self) + if event.name in cls.event_handlers: + handler = cls.event_handlers[event.name] + + # For background tasks, proxy the state + if handler.is_background: + return StateProxy(self), handler + + return self, handler + from reflex.event import get_event_handler from reflex.utils.exceptions import EventHandlerValueError diff --git a/tests/integration/test_decentralized_event_handlers.py b/tests/integration/test_decentralized_event_handlers.py index 87aed635caa..42f32a9a6a3 100644 --- a/tests/integration/test_decentralized_event_handlers.py +++ b/tests/integration/test_decentralized_event_handlers.py @@ -12,6 +12,7 @@ def DecentralizedEventHandlers(): class TestState(rx.State): count: int = 0 + value: int = 0 @rx.event def increment(self): @@ -36,11 +37,23 @@ def reset_count(state: TestState): """ state.count = 0 + @rx.event + def set_value(state: TestState, value: str): + """Set the value with a parameter. + + Args: + state: The state to modify. + value: The value to set. + """ + state.value = int(value) + def index(): return rx.vstack( rx.heading(TestState.count, id="counter"), + rx.heading(TestState.value, id="value"), rx.button("Increment", on_click=TestState.increment, id="increment"), rx.button("Reset", on_click=reset_count, id="reset"), + rx.button("Set Value", on_click=set_value("42"), id="set-value"), rx.text("Loaded", on_mount=on_load, id="loaded"), ) @@ -98,13 +111,19 @@ def test_decentralized_event_handlers( assert decentralized_handlers.app_instance is not None counter = driver.find_element(By.ID, "counter") + value = driver.find_element(By.ID, "value") increment_button = driver.find_element(By.ID, "increment") reset_button = driver.find_element(By.ID, "reset") + set_value_button = driver.find_element(By.ID, "set-value") assert decentralized_handlers._poll_for(lambda: counter.text == "10", timeout=5) + assert value.text == "0" increment_button.click() assert decentralized_handlers._poll_for(lambda: counter.text == "11", timeout=5) reset_button.click() assert decentralized_handlers._poll_for(lambda: counter.text == "0", timeout=5) + + set_value_button.click() + assert decentralized_handlers._poll_for(lambda: value.text == "42", timeout=5) From ee11e1a7847182fbbb0e3abb24a51ec9ab85708a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 29 Apr 2025 18:19:31 -0700 Subject: [PATCH 03/11] make ci run From 9cc0ae57522d34df2dfe22dbd7312371ce6afc81 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:31:15 +0000 Subject: [PATCH 04/11] Fix _get_event_handler to maintain backward compatibility Co-Authored-By: khaleel@reflex.dev --- reflex/state.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 040e9e26453..46b5ba08acc 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1591,12 +1591,12 @@ def _get_event_handler( Raises: ValueError: If the event handler or substate is not found. - EventHandlerValueError: If the event handler is not found. """ # Get the event handler. path = event.name.split(".") if "." not in event.name: + # Check if the event handler exists in the class's event_handlers cls = type(self) if event.name in cls.event_handlers: handler = cls.event_handlers[event.name] @@ -1608,17 +1608,14 @@ def _get_event_handler( return self, handler from reflex.event import get_event_handler - from reflex.utils.exceptions import EventHandlerValueError handler = get_event_handler(event.name) - if handler is None: - raise EventHandlerValueError(f"Event handler {event.name} not found.") - - # For background tasks, proxy the state - if handler.is_background: - return StateProxy(self), handler + if handler is not None: + # For background tasks, proxy the state + if handler.is_background: + return StateProxy(self), handler - return self, handler + return self, handler path, name = path[:-1], path[-1] substate = self.get_substate(path) From e1d07b4e392bc31d168cf901f7c9b7e2402cdcbe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:34:15 +0000 Subject: [PATCH 05/11] Fix docstring for wrap_decentralized_handler Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index 9075fd7fa89..c612eb2a23e 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -1583,7 +1583,19 @@ def wrap_decentralized_handler(fn: Callable) -> Callable: Returns: A wrapped function that can be used with component events. + + Raises: + ValueError: If the event handler doesn't have at least one parameter (state). """ + # Get the signature of the function to determine parameter names + sig = inspect.signature(fn) + param_names = list(sig.parameters.keys()) + + # The first parameter should be the state parameter + if not param_names: + raise ValueError( + f"Event handler {fn.__name__} must have at least one parameter (state)" + ) # Create a wrapper function that doesn't require the state parameter def wrapper(*args, **kwargs): @@ -1599,12 +1611,22 @@ def wrapper(*args, **kwargs): # Create an event spec with the provided arguments arg_specs = [] + # Skip the first parameter (state) when creating arg specs + param_offset = 1 # Skip the state parameter + for i, arg in enumerate(args): # Create a var for the arg var_arg = Var.create(arg) + # Get the parameter name if available, otherwise use a generic name + param_name = ( + param_names[i + param_offset] + if i + param_offset < len(param_names) + else f"arg{i}" + ) + # Add the arg to the arg specs - arg_specs.append((Var.create_safe(f"arg{i}"), var_arg)) + arg_specs.append((Var.create_safe(param_name), var_arg)) for name, arg in kwargs.items(): # Create a var for the arg From 5825564ee3b41ce8df929171ca995b600c622a9e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:36:11 +0000 Subject: [PATCH 06/11] Fix parameter handling for decentralized event handlers Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index c612eb2a23e..6260d138330 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -1673,8 +1673,11 @@ def call_event_fn( # Check if this is a decentralized event handler if is_decentralized_event_handler(fn): wrapped_fn = wrap_decentralized_handler(fn) - # Call the wrapped function directly without passing arg_spec - return [wrapped_fn()] + + parsed_args = parse_args_spec(arg_spec) + + # Call the wrapped function with the parsed arguments + return [wrapped_fn(*parsed_args)] # Check that fn signature matches arg_spec check_fn_match_arg_spec(fn, arg_spec, key=key) From 876c8e4001bf2ba8ab8435490e2ec90c4e89a980 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:38:16 +0000 Subject: [PATCH 07/11] Fix decentralized event handlers with parameters Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 6260d138330..7db427c2f28 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -2213,8 +2213,12 @@ def wrapper( setattr(handler, BACKGROUND_TASK_MARKER, True) # Mark the function as a decentralized event handler setattr(func, DECENTRALIZED_EVENT_MARKER, True) - # Return the original function so it can be called normally - return func # pyright: ignore [reportReturnType] + + # Create a wrapped version that can handle parameters + wrapped = wrap_decentralized_handler(func) + + # Return the wrapped function instead of the original + return wrapped # pyright: ignore [reportReturnType] if func is not None: return wrapper(func) From f12a9c72782d335e896ebcfa93a0f02a8ec15374 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:46:46 +0000 Subject: [PATCH 08/11] Fix decentralized event handler marker preservation Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 7db427c2f28..a2451bd3bc0 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -527,13 +527,19 @@ def create( # If the input is a callable, create an event chain. elif isinstance(value, Callable): - result = call_event_fn(value, args_spec, key=key) - if isinstance(result, Var): - # Recursively call this function if the lambda returned an EventChain Var. - return cls.create( - value=result, args_spec=args_spec, key=key, **event_chain_kwargs - ) - events = [*result] + # Check if this is a decentralized event handler + if is_decentralized_event_handler(value): + wrapped_fn = wrap_decentralized_handler(value) + # Create an event spec directly + events = [wrapped_fn()] + else: + result = call_event_fn(value, args_spec, key=key) + if isinstance(result, Var): + # Recursively call this function if the lambda returned an EventChain Var. + return cls.create( + value=result, args_spec=args_spec, key=key, **event_chain_kwargs + ) + events = [*result] # Otherwise, raise an error. else: @@ -1330,13 +1336,14 @@ def call_event_handler( # Handle partial application of EventSpec args return event_callback.add_args(*event_spec_args) - check_fn_match_arg_spec( - event_callback.fn, - event_spec, - key, - bool(event_callback.state_full_name), - event_callback.fn.__qualname__, - ) + if not is_decentralized_event_handler(event_callback.fn): + check_fn_match_arg_spec( + event_callback.fn, + event_spec, + key, + bool(event_callback.state_full_name), + event_callback.fn.__qualname__, + ) all_acceptable_specs = ( [event_spec] if not isinstance(event_spec, Sequence) else event_spec @@ -1642,6 +1649,10 @@ def wrapper(*args, **kwargs): wrapper.__doc__ = fn.__doc__ wrapper.__module__ = fn.__module__ + # Preserve the decentralized event marker + if hasattr(fn, DECENTRALIZED_EVENT_MARKER): + setattr(wrapper, DECENTRALIZED_EVENT_MARKER, True) + return wrapper @@ -1679,8 +1690,9 @@ def call_event_fn( # Call the wrapped function with the parsed arguments return [wrapped_fn(*parsed_args)] - # Check that fn signature matches arg_spec - check_fn_match_arg_spec(fn, arg_spec, key=key) + # Check that fn signature matches arg_spec (skip for decentralized event handlers) + if not is_decentralized_event_handler(fn): + check_fn_match_arg_spec(fn, arg_spec, key=key) parsed_args = parse_args_spec(arg_spec) From 6583f9289e241e72beb080b66eb89f25f3540ca1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 02:02:08 +0000 Subject: [PATCH 09/11] Add simple test for decentralized event handlers with proper docstrings Co-Authored-By: khaleel@reflex.dev --- test_decentralized_handlers_simple.py | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test_decentralized_handlers_simple.py diff --git a/test_decentralized_handlers_simple.py b/test_decentralized_handlers_simple.py new file mode 100644 index 00000000000..2d8bed41a33 --- /dev/null +++ b/test_decentralized_handlers_simple.py @@ -0,0 +1,48 @@ +"""Simple test for decentralized event handlers.""" + +import reflex as rx + + +class TestState(rx.State): + """Test state class for decentralized event handlers.""" + + count: int = 0 + + +@rx.event +def reset_count(state: TestState): + """Reset the count to zero. + + Args: + state: The test state to modify. + """ + state.count = 0 + + +@rx.event +def set_count(state: TestState, value: str): + """Set the count to a specific value. + + Args: + state: The test state to modify. + value: The value to set as count. + """ + state.count = int(value) + + +def test_is_decentralized(): + """Test if functions are correctly identified as decentralized event handlers.""" + from reflex.event import is_decentralized_event_handler, wrap_decentralized_handler + + assert is_decentralized_event_handler(reset_count) + + wrapped = wrap_decentralized_handler(reset_count) + assert is_decentralized_event_handler(wrapped) + + assert is_decentralized_event_handler(set_count) + wrapped_with_params = wrap_decentralized_handler(set_count) + assert is_decentralized_event_handler(wrapped_with_params) + + +if __name__ == "__main__": + test_is_decentralized() From dde12b73f6581bc557b3ba18cf4b2a79f23f9f41 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 02:19:38 +0000 Subject: [PATCH 10/11] Fix decentralized event handler marker preservation Co-Authored-By: khaleel@reflex.dev --- reflex/event.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index a2451bd3bc0..bb8898907d8 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -1650,8 +1650,7 @@ def wrapper(*args, **kwargs): wrapper.__module__ = fn.__module__ # Preserve the decentralized event marker - if hasattr(fn, DECENTRALIZED_EVENT_MARKER): - setattr(wrapper, DECENTRALIZED_EVENT_MARKER, True) + setattr(wrapper, DECENTRALIZED_EVENT_MARKER, True) return wrapper From 6e98a68d0d58cb39d4fb9c2089447057f582cef4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 02:24:04 +0000 Subject: [PATCH 11/11] Move test_decentralized_handlers_simple.py to tests/units directory Co-Authored-By: khaleel@reflex.dev --- .../units/test_decentralized_handlers_simple.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test_decentralized_handlers_simple.py => tests/units/test_decentralized_handlers_simple.py (100%) diff --git a/test_decentralized_handlers_simple.py b/tests/units/test_decentralized_handlers_simple.py similarity index 100% rename from test_decentralized_handlers_simple.py rename to tests/units/test_decentralized_handlers_simple.py