Browse Source

Style props with Callable Values (#1751)

Elijah Ahianyo 1 year ago
parent
commit
06a110a07d
3 changed files with 63 additions and 5 deletions
  1. 7 0
      reflex/utils/exceptions.py
  2. 22 5
      reflex/utils/format.py
  3. 34 0
      tests/test_utils.py

+ 7 - 0
reflex/utils/exceptions.py

@@ -0,0 +1,7 @@
+"""Custom Exceptions."""
+
+
+class InvalidStylePropError(TypeError):
+    """Custom Type Error when style props have invalid values."""
+
+    pass

+ 22 - 5
reflex/utils/format.py

@@ -9,14 +9,15 @@ import os
 import os.path as op
 import os.path as op
 import re
 import re
 import sys
 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
 import plotly.graph_objects as go
 from plotly.graph_objects import Figure
 from plotly.graph_objects import Figure
 from plotly.io import to_json
 from plotly.io import to_json
 
 
 from reflex import constants
 from reflex import constants
-from reflex.utils import types
+from reflex.utils import exceptions, types
 from reflex.vars import Var
 from reflex.vars import Var
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -288,7 +289,8 @@ def format_prop(
         The formatted prop to display within a tag.
         The formatted prop to display within a tag.
 
 
     Raises:
     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.
     # import here to avoid circular import.
     from reflex.event import EVENT_ARG, EventChain
     from reflex.event import EVENT_ARG, EventChain
@@ -324,6 +326,8 @@ def format_prop(
         else:
         else:
             # Dump the prop as JSON.
             # Dump the prop as JSON.
             prop = json_dumps(prop)
             prop = json_dumps(prop)
+    except exceptions.InvalidStylePropError:
+        raise
     except TypeError as e:
     except TypeError as e:
         raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from 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:
     Returns:
         The formatted dict.
         The formatted dict.
+
+    Raises:
+        InvalidStylePropError: If a style prop has a callable value
     """
     """
     # Import here to avoid circular imports.
     # Import here to avoid circular imports.
+    from reflex.event import EventHandler
     from reflex.vars import Var
     from reflex.vars import Var
 
 
+    prop_dict = {}
+
     # Convert any var keys to strings.
     # 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.
     # 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:
     def unescape_double_quotes_in_var(m: re.Match) -> str:
         # Since the outer quotes are removed, the inner escaped quotes must be unescaped.
         # Since the outer quotes are removed, the inner escaped quotes must be unescaped.

+ 34 - 0
tests/test_utils.py

@@ -11,6 +11,7 @@ from reflex import constants
 from reflex.base import Base
 from reflex.base import Base
 from reflex.components.tags import Tag
 from reflex.components.tags import Tag
 from reflex.event import EVENT_ARG, EventChain, EventHandler, EventSpec
 from reflex.event import EVENT_ARG, EventChain, EventHandler, EventSpec
+from reflex.state import State
 from reflex.style import Style
 from reflex.style import Style
 from reflex.utils import (
 from reflex.utils import (
     build,
     build,
@@ -45,6 +46,18 @@ V056 = version.parse("0.5.6")
 VMAXPLUS1 = version.parse(get_above_max_version())
 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(
 @pytest.mark.parametrize(
     "input,output",
     "input,output",
     [
     [
@@ -744,3 +757,24 @@ def test_output_system_info(mocker):
     """
     """
     mocker.patch("reflex.utils.console.LOG_LEVEL", constants.LogLevel.DEBUG)
     mocker.patch("reflex.utils.console.LOG_LEVEL", constants.LogLevel.DEBUG)
     utils_exec.output_system_info()
     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