diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index 39f347196..855b13056 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -2,20 +2,13 @@ from __future__ import annotations -import json -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union - -from plotly.graph_objects import Figure -from plotly.io import to_json +from typing import Any, Dict, List, Optional, Set, Tuple, Union from reflex.base import Base -from reflex.event import EVENT_ARG, EventChain +from reflex.event import EventChain from reflex.utils import format, types from reflex.vars import Var -if TYPE_CHECKING: - from reflex.components.component import ComponentStyle - class Tag(Base): """A React tag.""" @@ -52,61 +45,6 @@ class Tag(Base): } super().__init__(*args, **kwargs) - @staticmethod - def format_prop( - prop: Union[Var, EventChain, ComponentStyle, str], - ) -> Union[int, float, str]: - """Format a prop. - - Args: - prop: The prop to format. - - Returns: - The formatted prop to display within a tag. - - Raises: - TypeError: If the prop is not a valid type. - """ - try: - # Handle var props. - if isinstance(prop, Var): - if not prop.is_local or prop.is_string: - return str(prop) - if types._issubclass(prop.type_, str): - return format.format_string(prop.full_name) - prop = prop.full_name - - # Handle event props. - elif isinstance(prop, EventChain): - chain = ",".join([format.format_event(event) for event in prop.events]) - event = f"Event([{chain}], {EVENT_ARG})" - prop = f"{EVENT_ARG} => {event}" - - # Handle other types. - elif isinstance(prop, str): - if format.is_wrapped(prop, "{"): - return prop - return format.json_dumps(prop) - - elif isinstance(prop, Figure): - prop = json.loads(to_json(prop))["data"] # type: ignore - - # For dictionaries, convert any properties to strings. - elif isinstance(prop, dict): - prop = format.format_dict(prop) - - else: - # Dump the prop as JSON. - prop = format.json_dumps(prop) - except TypeError as e: - raise TypeError( - f"Could not format prop: {prop} of type {type(prop)}" - ) from e - - # Wrap the variable in braces. - assert isinstance(prop, str), "The prop must be a string." - return format.wrap(prop, "{", check_first=False) - def format_props(self) -> List: """Format the tag's props. @@ -119,7 +57,7 @@ class Tag(Base): # Format all the props. return [ - f"{name}={self.format_prop(prop)}" + f"{name}={format.format_prop(prop)}" for name, prop in sorted(self.props.items()) if prop is not None ] + [str(prop) for prop in self.special_props] diff --git a/reflex/utils/format.py b/reflex/utils/format.py index b799b8a06..24eec9a83 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -9,9 +9,10 @@ import os import os.path as op import re import sys -from typing import TYPE_CHECKING, Any, Type +from typing import TYPE_CHECKING, Any, Type, Union import plotly.graph_objects as go +from plotly.graph_objects import Figure from plotly.io import to_json from reflex import constants @@ -258,6 +259,62 @@ def format_cond( return wrap(f"{cond} ? {true_value} : {false_value}", "{") +def format_prop( + prop: Union[Var, EventChain, ComponentStyle, str], +) -> Union[int, float, str]: + """Format a prop. + + Args: + prop: The prop to format. + + Returns: + The formatted prop to display within a tag. + + Raises: + TypeError: If the prop is not a valid type. + """ + # import here to avoid circular import. + from reflex.event import EVENT_ARG, EventChain + + try: + # Handle var props. + if isinstance(prop, Var): + if not prop.is_local or prop.is_string: + return str(prop) + if types._issubclass(prop.type_, str): + return format_string(prop.full_name) + prop = prop.full_name + + # Handle event props. + elif isinstance(prop, EventChain): + chain = ",".join([format_event(event) for event in prop.events]) + event = f"Event([{chain}], {EVENT_ARG})" + prop = f"{EVENT_ARG} => {event}" + + # Handle other types. + elif isinstance(prop, str): + if is_wrapped(prop, "{"): + return prop + return json_dumps(prop) + + elif isinstance(prop, Figure): + prop = json.loads(to_json(prop))["data"] # type: ignore + + # For dictionaries, convert any properties to strings. + elif isinstance(prop, dict): + prop = format_dict(prop) + + else: + # Dump the prop as JSON. + prop = json_dumps(prop) + except TypeError as e: + raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from e + + # Wrap the variable in braces. + assert isinstance(prop, str), "The prop must be a string." + return wrap(prop, "{", check_first=False) + + def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: """Get the state and function name of an event handler. diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py index 3f47891e6..7964838ad 100644 --- a/tests/components/test_tag.py +++ b/tests/components/test_tag.py @@ -1,90 +1,11 @@ -from typing import Any, Dict, List +from typing import Dict, List import pytest from reflex.components.tags import CondTag, Tag, tagless -from reflex.event import EVENT_ARG, EventChain, EventHandler, EventSpec -from reflex.style import Style from reflex.vars import BaseVar, Var -def mock_event(arg): - pass - - -@pytest.mark.parametrize( - "prop,formatted", - [ - ("string", '"string"'), - ("{wrapped_string}", "{wrapped_string}"), - (True, "{true}"), - (False, "{false}"), - (123, "{123}"), - (3.14, "{3.14}"), - ([1, 2, 3], "{[1, 2, 3]}"), - (["a", "b", "c"], '{["a", "b", "c"]}'), - ({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'), - ({"a": 'foo "bar" baz'}, r'{{"a": "foo \"bar\" baz"}}'), - ( - { - "a": 'foo "{ "bar" }" baz', - "b": BaseVar(name="val", type_="str"), - }, - r'{{"a": "foo \"{ \"bar\" }\" baz", "b": val}}', - ), - ( - EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]), - '{_e => Event([E("mock_event", {})], _e)}', - ), - ( - EventChain( - events=[ - EventSpec( - handler=EventHandler(fn=mock_event), - args=((Var.create_safe("arg"), EVENT_ARG.target.value),), - ) - ] - ), - '{_e => Event([E("mock_event", {arg:_e.target.value})], _e)}', - ), - ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'), - (BaseVar(name="var", type_="int"), "{var}"), - ( - BaseVar( - name="_", - type_=Any, - state="", - is_local=True, - is_string=False, - ), - "{_}", - ), - (BaseVar(name='state.colors["a"]', type_="str"), '{state.colors["a"]}'), - ({"a": BaseVar(name="val", type_="str")}, '{{"a": val}}'), - ({"a": BaseVar(name='"val"', type_="str")}, '{{"a": "val"}}'), - ( - {"a": BaseVar(name='state.colors["val"]', type_="str")}, - '{{"a": state.colors["val"]}}', - ), - # tricky real-world case from markdown component - ( - { - "h1": f"{{({{node, ...props}}) => }}" - }, - '{{"h1": ({node, ...props}) => }}', - ), - ], -) -def test_format_prop(prop: Var, formatted: str): - """Test that the formatted value of an prop is correct. - - Args: - prop: The prop to test. - formatted: The expected formatted value. - """ - assert Tag.format_prop(prop) == formatted - - @pytest.mark.parametrize( "props,test_props", [ diff --git a/tests/test_utils.py b/tests/test_utils.py index c93b7f1b8..5b16af198 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,6 +9,9 @@ from packaging import version 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.style import Style from reflex.utils import ( build, format, @@ -17,7 +20,11 @@ from reflex.utils import ( types, ) from reflex.utils import exec as utils_exec -from reflex.vars import Var +from reflex.vars import BaseVar, Var + + +def mock_event(arg): + pass def get_above_max_version(): @@ -262,6 +269,79 @@ def test_format_route(route: str, expected: bool): assert format.format_route(route) == expected +@pytest.mark.parametrize( + "prop,formatted", + [ + ("string", '"string"'), + ("{wrapped_string}", "{wrapped_string}"), + (True, "{true}"), + (False, "{false}"), + (123, "{123}"), + (3.14, "{3.14}"), + ([1, 2, 3], "{[1, 2, 3]}"), + (["a", "b", "c"], '{["a", "b", "c"]}'), + ({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'), + ({"a": 'foo "bar" baz'}, r'{{"a": "foo \"bar\" baz"}}'), + ( + { + "a": 'foo "{ "bar" }" baz', + "b": BaseVar(name="val", type_="str"), + }, + r'{{"a": "foo \"{ \"bar\" }\" baz", "b": val}}', + ), + ( + EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]), + '{_e => Event([E("mock_event", {})], _e)}', + ), + ( + EventChain( + events=[ + EventSpec( + handler=EventHandler(fn=mock_event), + args=((Var.create_safe("arg"), EVENT_ARG.target.value),), + ) + ] + ), + '{_e => Event([E("mock_event", {arg:_e.target.value})], _e)}', + ), + ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'), + (BaseVar(name="var", type_="int"), "{var}"), + ( + BaseVar( + name="_", + type_=Any, + state="", + is_local=True, + is_string=False, + ), + "{_}", + ), + (BaseVar(name='state.colors["a"]', type_="str"), '{state.colors["a"]}'), + ({"a": BaseVar(name="val", type_="str")}, '{{"a": val}}'), + ({"a": BaseVar(name='"val"', type_="str")}, '{{"a": "val"}}'), + ( + {"a": BaseVar(name='state.colors["val"]', type_="str")}, + '{{"a": state.colors["val"]}}', + ), + # tricky real-world case from markdown component + ( + { + "h1": f"{{({{node, ...props}}) => }}" + }, + '{{"h1": ({node, ...props}) => }}', + ), + ], +) +def test_format_prop(prop: Var, formatted: str): + """Test that the formatted value of an prop is correct. + + Args: + prop: The prop to test. + formatted: The expected formatted value. + """ + assert format.format_prop(prop) == formatted + + def test_validate_invalid_bun_path(mocker): """Test that an error is thrown when a custom specified bun path is not valid or does not exist.