From 49e8eeecdd53557a62445768b702cbc407157b56 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:42:09 +0100 Subject: [PATCH 01/14] added custom exception handler --- reflex/app.py | 3 ++- reflex/state.py | 15 ++++++++++++++- reflex/utils/exceptions.py | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 7ab4b5abe0..34e9533285 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -141,7 +141,7 @@ class App(Base): # The radix theme for the entire app theme: Optional[Component] = themes.theme(accent_color="blue") - def __init__(self, *args, **kwargs): + def __init__(self, exception_handler = None, *args, **kwargs): """Initialize the app. Args: @@ -158,6 +158,7 @@ def __init__(self, *args, **kwargs): "`connect_error_component` is deprecated, use `overlay_component` instead" ) super().__init__(*args, **kwargs) + BaseState.set_exception_handler(exception_handler) state_subclasses = BaseState.__subclasses__() is_testing_env = constants.PYTEST_CURRENT_TEST in os.environ diff --git a/reflex/state.py b/reflex/state.py index 33ff12c177..d41173501a 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -213,6 +213,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # The router data for the current page router: RouterData = RouterData() + # Custom exception hanlder + _exception_handler = None + + @classmethod + def set_exception_handler(cls, exception_handler): + cls._exception_handler = exception_handler + + @classmethod + def _handle_exception(cls, exception): + if cls._exception_handler: + cls._exception_handler(exception) + def __init__(self, *args, parent_state: BaseState | None = None, **kwargs): """Initialize the state. @@ -1192,7 +1204,8 @@ async def _process_event( # If an error occurs, throw a window alert. except Exception: error = traceback.format_exc() - print(error) + # print(error) + BaseState._handle_exception(error) yield state._as_state_update( handler, window_alert("An error occurred. See logs for details."), diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index e4a9a6e6c9..63ab9cecae 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -19,3 +19,11 @@ class MatchTypeError(TypeError): """Raised when the return types of match cases are different.""" pass + +class ExceptionHandler: + @staticmethod + def handle_exception(exception_handler, exception): + if exception_handler: + exception_handler(exception) + else: + print(exception) \ No newline at end of file From 8082062768e4581bd1a00a68a0650cd4431420e2 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:45:01 +0100 Subject: [PATCH 02/14] added comment in exceptions.py --- reflex/utils/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 63ab9cecae..209cde3d6b 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -21,6 +21,8 @@ class MatchTypeError(TypeError): pass class ExceptionHandler: + ''' this is another idea i thought of making an exception handler class. state.py and app.py + will use it to handle exception during event handling ''' @staticmethod def handle_exception(exception_handler, exception): if exception_handler: From 81df28596cf24df4ac046e9df249c4c76532c417 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:02:33 +0100 Subject: [PATCH 03/14] exception_handler added as rx.App attribute --- reflex/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 34e9533285..7dcca21f01 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -141,7 +141,10 @@ class App(Base): # The radix theme for the entire app theme: Optional[Component] = themes.theme(accent_color="blue") - def __init__(self, exception_handler = None, *args, **kwargs): + # Custom exception handler + exception_handler: Optional[Callable] = None + + def __init__(self, *args, **kwargs): """Initialize the app. Args: @@ -158,7 +161,6 @@ def __init__(self, exception_handler = None, *args, **kwargs): "`connect_error_component` is deprecated, use `overlay_component` instead" ) super().__init__(*args, **kwargs) - BaseState.set_exception_handler(exception_handler) state_subclasses = BaseState.__subclasses__() is_testing_env = constants.PYTEST_CURRENT_TEST in os.environ From 56044543f7422f6374f91c4d34bc2abc3e6aa933 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:05:19 +0100 Subject: [PATCH 04/14] app instance is used to get exception_handler and default behavior of exception is added --- reflex/state.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index d41173501a..498a31af5f 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -213,18 +213,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # The router data for the current page router: RouterData = RouterData() - # Custom exception hanlder - _exception_handler = None - - @classmethod - def set_exception_handler(cls, exception_handler): - cls._exception_handler = exception_handler - - @classmethod - def _handle_exception(cls, exception): - if cls._exception_handler: - cls._exception_handler(exception) - def __init__(self, *args, parent_state: BaseState | None = None, **kwargs): """Initialize the state. @@ -1204,8 +1192,12 @@ async def _process_event( # If an error occurs, throw a window alert. except Exception: error = traceback.format_exc() - # print(error) - BaseState._handle_exception(error) + app = getattr(prerequisites.get_app(), constants.CompileVars.APP) + exception_handler = app.exception_handler + if callable(exception_handler): + exception_handler(error) + else: + print(error) yield state._as_state_update( handler, window_alert("An error occurred. See logs for details."), From a0382ac71237464c8ec92e6da5b136f9748511ca Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:10:38 +0100 Subject: [PATCH 05/14] added exception_handler for rx.call_script() for client side exception handling --- reflex/event.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 499b4877a6..2ed2952d36 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -641,12 +641,14 @@ def _callback_arg_spec(eval_result): def call_script( javascript_code: str, callback: EventHandler | Callable | None = None, + exception_handler: callable | None = None ) -> EventSpec: """Create an event handler that executes arbitrary javascript code. Args: javascript_code: The code to execute. callback: EventHandler that will receive the result of evaluating the javascript code. + exception_handler: User-defined exception handler Returns: EventSpec: An event that will execute the client side javascript. @@ -666,12 +668,19 @@ def call_script( callback_kwargs = { "callback": f"({arg_name}) => queueEvents([{format.format_event(event_spec)}], {constants.CompileVars.SOCKET})" } - return server_side( - "_call_script", - get_fn_signature(call_script), - javascript_code=javascript_code, - **callback_kwargs, - ) + + try: + return server_side( + "_call_script", + get_fn_signature(call_script), + javascript_code=javascript_code, + **callback_kwargs, + ) + except Exception as client_error: + if exception_handler and callable(exception_handler): + exception_handler(client_error) + else: + raise client_error def get_event(state, event): From 047b283246f5400d8b23c624c6319d9de7a297a2 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:16:02 +0100 Subject: [PATCH 06/14] ExceptionHandler class removed from exceptions.py --- reflex/utils/exceptions.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 209cde3d6b..e4a9a6e6c9 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -19,13 +19,3 @@ class MatchTypeError(TypeError): """Raised when the return types of match cases are different.""" pass - -class ExceptionHandler: - ''' this is another idea i thought of making an exception handler class. state.py and app.py - will use it to handle exception during event handling ''' - @staticmethod - def handle_exception(exception_handler, exception): - if exception_handler: - exception_handler(exception) - else: - print(exception) \ No newline at end of file From 4613033bbc5ca929e7c0bcebe00bd4dfacd1067b Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:47:32 +0100 Subject: [PATCH 07/14] removed exception handler from call_script and try/catch added to catch frontend exception --- reflex/event.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 2ed2952d36..243a311af1 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -641,7 +641,6 @@ def _callback_arg_spec(eval_result): def call_script( javascript_code: str, callback: EventHandler | Callable | None = None, - exception_handler: callable | None = None ) -> EventSpec: """Create an event handler that executes arbitrary javascript code. @@ -668,19 +667,23 @@ def call_script( callback_kwargs = { "callback": f"({arg_name}) => queueEvents([{format.format_event(event_spec)}], {constants.CompileVars.SOCKET})" } + wrapped_code = f""" + try {{ + {javascript_code} + }} catch (error) {{ + queueEvents([{window_alert("'An error occurred. See logs for details.'")}], {constants.CompileVars.SOCKET}); + }} + """ + - try: - return server_side( + + return server_side( "_call_script", get_fn_signature(call_script), - javascript_code=javascript_code, + javascript_code=wrapped_code, **callback_kwargs, ) - except Exception as client_error: - if exception_handler and callable(exception_handler): - exception_handler(client_error) - else: - raise client_error + def get_event(state, event): From bc72f196815709b0662e6c17ea0f058eb17a0c9f Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:47:32 +0100 Subject: [PATCH 08/14] wrapped_code removed --- reflex/event.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 243a311af1..3d98f5ee03 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -647,7 +647,6 @@ def call_script( Args: javascript_code: The code to execute. callback: EventHandler that will receive the result of evaluating the javascript code. - exception_handler: User-defined exception handler Returns: EventSpec: An event that will execute the client side javascript. @@ -667,25 +666,15 @@ def call_script( callback_kwargs = { "callback": f"({arg_name}) => queueEvents([{format.format_event(event_spec)}], {constants.CompileVars.SOCKET})" } - wrapped_code = f""" - try {{ - {javascript_code} - }} catch (error) {{ - queueEvents([{window_alert("'An error occurred. See logs for details.'")}], {constants.CompileVars.SOCKET}); - }} - """ - - - + return server_side( "_call_script", get_fn_signature(call_script), - javascript_code=wrapped_code, + javascript_code=javascript_code, **callback_kwargs, ) - def get_event(state, event): """Get the event from the given state. From f44c0bc0cb118151e2e0706d688f055c63cff77a Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Fri, 23 Feb 2024 08:44:52 +0100 Subject: [PATCH 09/14] js try/catch & queueEvents added --- reflex/event.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 3d98f5ee03..5434b3c500 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -666,15 +666,25 @@ def call_script( callback_kwargs = { "callback": f"({arg_name}) => queueEvents([{format.format_event(event_spec)}], {constants.CompileVars.SOCKET})" } - + error_event_spec= None + wrapped_code = f""" + try {{ + {javascript_code} + }} catch (error) {{ + console.log(error) + queueEvents([{format.format_event(error_event_spec)}], {constants.CompileVars.SOCKET}) + }} +""" + return server_side( "_call_script", get_fn_signature(call_script), - javascript_code=javascript_code, + javascript_code=wrapped_code, **callback_kwargs, ) + def get_event(state, event): """Get the event from the given state. From 9ae325c37661eefec6d3c1718ed0eae3d05b038b Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:13:06 +0100 Subject: [PATCH 10/14] error event created --- reflex/__init__.py | 1 + reflex/event.py | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/reflex/__init__.py b/reflex/__init__.py index 276c05b993..2ee87352fb 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -132,6 +132,7 @@ "EventChain", "background", "call_script", + "client_error", "clear_local_storage", "console_log", "download", diff --git a/reflex/event.py b/reflex/event.py index 5434b3c500..2d03558fb7 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -18,7 +18,7 @@ from reflex import constants from reflex.base import Base -from reflex.utils import console, format +from reflex.utils import console, format, prerequisites from reflex.utils.types import ArgsSpec from reflex.vars import BaseVar, Var @@ -654,6 +654,7 @@ def call_script( Raises: ValueError: If the callback is not a valid event handler. """ + wrapped_code = None callback_kwargs = {} if callback is not None: arg_name = parse_args_spec(_callback_arg_spec)[0]._var_name @@ -666,16 +667,16 @@ def call_script( callback_kwargs = { "callback": f"({arg_name}) => queueEvents([{format.format_event(event_spec)}], {constants.CompileVars.SOCKET})" } - error_event_spec= None + + error_event_spec = client_error('client error') + wrapped_code = f""" try {{ {javascript_code} - }} catch (error) {{ - console.log(error) - queueEvents([{format.format_event(error_event_spec)}], {constants.CompileVars.SOCKET}) + }} catch (e) {{ + queueEvents([{format.format_event(error_event_spec)}], {constants.CompileVars.SOCKET}) }} -""" - + """ return server_side( "_call_script", get_fn_signature(call_script), @@ -683,7 +684,17 @@ def call_script( **callback_kwargs, ) +def client_error(error: Any) -> EventSpec: + return server_side( + "_client_error", + get_fn_signature(client_error), + error = error + ) +def client_error_handler(): # just for test + app = getattr(prerequisites.get_app(), constants.CompileVars.APP) + exception_handler = app.exception_handler + return exception_handler def get_event(state, event): """Get the event from the given state. From c7ee5d16e0aebbfdd0540cc2c5b03ee38f1ce78a Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:13:48 +0100 Subject: [PATCH 11/14] on_error added to add_page to pass custom exception handler --- reflex/app.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/reflex/app.py b/reflex/app.py index 7dcca21f01..e665f7a295 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -144,6 +144,9 @@ class App(Base): # Custom exception handler exception_handler: Optional[Callable] = None + # client-side exception handler on error + load_error_handler: Optional[Callable] = None + def __init__(self, *args, **kwargs): """Initialize the app. @@ -394,6 +397,10 @@ def add_page( | None = None, meta: list[dict[str, str]] = constants.DefaultPage.META_LIST, script_tags: list[Component] | None = None, + on_error: EventHandler + | EventSpec + | list[EventHandler | EventSpec] + | None = None, ): """Add a page to the app. @@ -467,6 +474,11 @@ def add_page( if not isinstance(on_load, list): on_load = [on_load] self.load_events[route] = on_load + + if on_error: + self.load_error_handler = on_error + + def get_load_events(self, route: str) -> list[EventHandler | EventSpec]: """Get the load events for a route. From 98762071f6c1a6020599bb4f826c1ecf20ddcc39 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:23:41 +0100 Subject: [PATCH 12/14] removed line --- reflex/event.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index 2d03558fb7..2e96843747 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -654,7 +654,6 @@ def call_script( Raises: ValueError: If the callback is not a valid event handler. """ - wrapped_code = None callback_kwargs = {} if callback is not None: arg_name = parse_args_spec(_callback_arg_spec)[0]._var_name From 1e0948f2511284734372f36ac7b2a95914e82e86 Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:59:52 +0100 Subject: [PATCH 13/14] fixed error handler --- reflex/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index 2e96843747..c634915f59 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -692,7 +692,7 @@ def client_error(error: Any) -> EventSpec: def client_error_handler(): # just for test app = getattr(prerequisites.get_app(), constants.CompileVars.APP) - exception_handler = app.exception_handler + exception_handler = app.load_error_handler return exception_handler def get_event(state, event): From b418b2105cc9f9daa02dc20452f5588eece7358f Mon Sep 17 00:00:00 2001 From: Tanvir Riyad <113178352+tanvirriyad@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:21:33 +0100 Subject: [PATCH 14/14] test added for custom exception handler --- tests/states/__init__.py | 3 +++ tests/test_app.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/tests/states/__init__.py b/tests/states/__init__.py index 11e891ab4e..7058760a27 100644 --- a/tests/states/__init__.py +++ b/tests/states/__init__.py @@ -28,6 +28,9 @@ def go(self, c: int): Yields: After each increment. """ + if c < 0: + raise ValueError("Test exception") + for _ in range(c): self.value += 1 yield diff --git a/tests/test_app.py b/tests/test_app.py index 08b24b2ea6..4e6a2d99ae 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1131,6 +1131,24 @@ async def test_process_events(mocker, token: str): await app.state_manager.close() +@pytest.mark.asyncio +async def test_custom_exception_handler(): + + def custom_exception_handler(exception): + print(f"custom exception handler called: {exception}") + + app = App(state=GenState, exception_handler=custom_exception_handler) + event = Event( + token="t", + name="gen_state.go", + payload={"c": -1}, + ) + + with pytest.raises(ValueError, match="Test exception"): + async for _update in app.process(event, "mock_sid", {}, "127.0.0.1"): + pass + + @pytest.mark.parametrize( ("state", "overlay_component", "exp_page_child"), [