From 06a110a07ddd63d802dd34089d6a52fd93bea3cf Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Tue, 12 Sep 2023 18:26:53 +0000 Subject: [PATCH] Style props with Callable Values (#1751) --- reflex/utils/exceptions.py | 7 +++++++ reflex/utils/format.py | 27 ++++++++++++++++++++++----- tests/test_utils.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 reflex/utils/exceptions.py diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py new file mode 100644 index 000000000..542fe5a54 --- /dev/null +++ b/reflex/utils/exceptions.py @@ -0,0 +1,7 @@ +"""Custom Exceptions.""" + + +class InvalidStylePropError(TypeError): + """Custom Type Error when style props have invalid values.""" + + pass diff --git a/reflex/utils/format.py b/reflex/utils/format.py index e373331cf..2e68de82a 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -9,14 +9,15 @@ import os import os.path as op import re import sys -from typing import TYPE_CHECKING, Any, Type, Union +import types as builtin_types +from typing import TYPE_CHECKING, Any, Callable, Type, Union import plotly.graph_objects as go from plotly.graph_objects import Figure from plotly.io import to_json from reflex import constants -from reflex.utils import types +from reflex.utils import exceptions, types from reflex.vars import Var if TYPE_CHECKING: @@ -288,7 +289,8 @@ def format_prop( The formatted prop to display within a tag. Raises: - TypeError: If the prop is not a valid type. + exceptions.InvalidStylePropError: If the style prop value is not a valid type. + TypeError: If the prop is not valid. """ # import here to avoid circular import. from reflex.event import EVENT_ARG, EventChain @@ -324,6 +326,8 @@ def format_prop( else: # Dump the prop as JSON. prop = json_dumps(prop) + except exceptions.InvalidStylePropError: + raise except TypeError as e: raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from e @@ -584,15 +588,28 @@ def format_dict(prop: ComponentStyle) -> str: Returns: The formatted dict. + + Raises: + InvalidStylePropError: If a style prop has a callable value """ # Import here to avoid circular imports. + from reflex.event import EventHandler from reflex.vars import Var + prop_dict = {} + # Convert any var keys to strings. - prop = {key: str(val) if isinstance(val, Var) else val for key, val in prop.items()} + for key, value in prop.items(): + if issubclass(type(value), Callable): + raise exceptions.InvalidStylePropError( + f"The style prop `{to_snake_case(key)}` cannot have " # type: ignore + f"`{value.fn.__qualname__ if isinstance(value, EventHandler) else value.__qualname__ if isinstance(value, builtin_types.FunctionType) else value}`, " + f"an event handler or callable as its value" + ) + prop_dict[key] = str(value) if isinstance(value, Var) else value # Dump the dict to a string. - fprop = json_dumps(prop) + fprop = json_dumps(prop_dict) def unescape_double_quotes_in_var(m: re.Match) -> str: # Since the outer quotes are removed, the inner escaped quotes must be unescaped. diff --git a/tests/test_utils.py b/tests/test_utils.py index 552b046e5..8dd61a650 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,6 +11,7 @@ from reflex import constants from reflex.base import Base from reflex.components.tags import Tag from reflex.event import EVENT_ARG, EventChain, EventHandler, EventSpec +from reflex.state import State from reflex.style import Style from reflex.utils import ( build, @@ -45,6 +46,18 @@ V056 = version.parse("0.5.6") VMAXPLUS1 = version.parse(get_above_max_version()) +class ExampleTestState(State): + """Test state class.""" + + def test_event_handler(self): + """Test event handler.""" + pass + + +def test_func(): + pass + + @pytest.mark.parametrize( "input,output", [ @@ -744,3 +757,24 @@ def test_output_system_info(mocker): """ mocker.patch("reflex.utils.console.LOG_LEVEL", constants.LogLevel.DEBUG) utils_exec.output_system_info() + + +@pytest.mark.parametrize( + "callable", [ExampleTestState.test_event_handler, test_func, lambda x: x] +) +def test_style_prop_with_event_handler_value(callable): + """Test that a type error is thrown when a style prop has a + callable as value. + + Args: + callable: The callable function or event handler. + + """ + style = { + "color": EventHandler(fn=callable) + if type(callable) != EventHandler + else callable + } + + with pytest.raises(TypeError): + format.format_dict(style) # type: ignore