Refactor: Move format_prop Static Method for Improved Reusability (#1714)

This commit is contained in:
Elijah Ahianyo 2023-09-01 20:01:11 +00:00 committed by GitHub
parent 1d9f25be6d
commit 829a7751b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 147 deletions

View File

@ -2,20 +2,13 @@
from __future__ import annotations from __future__ import annotations
import json from typing import Any, Dict, List, Optional, Set, Tuple, Union
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 reflex.base import Base 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.utils import format, types
from reflex.vars import Var from reflex.vars import Var
if TYPE_CHECKING:
from reflex.components.component import ComponentStyle
class Tag(Base): class Tag(Base):
"""A React tag.""" """A React tag."""
@ -52,61 +45,6 @@ class Tag(Base):
} }
super().__init__(*args, **kwargs) 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: def format_props(self) -> List:
"""Format the tag's props. """Format the tag's props.
@ -119,7 +57,7 @@ class Tag(Base):
# Format all the props. # Format all the props.
return [ return [
f"{name}={self.format_prop(prop)}" f"{name}={format.format_prop(prop)}"
for name, prop in sorted(self.props.items()) for name, prop in sorted(self.props.items())
if prop is not None if prop is not None
] + [str(prop) for prop in self.special_props] ] + [str(prop) for prop in self.special_props]

View File

@ -9,9 +9,10 @@ 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 from typing import TYPE_CHECKING, Any, Type, Union
import plotly.graph_objects as go import plotly.graph_objects as go
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
@ -258,6 +259,62 @@ def format_cond(
return wrap(f"{cond} ? {true_value} : {false_value}", "{") 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]: def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
"""Get the state and function name of an event handler. """Get the state and function name of an event handler.

View File

@ -1,90 +1,11 @@
from typing import Any, Dict, List from typing import Dict, List
import pytest import pytest
from reflex.components.tags import CondTag, Tag, tagless 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 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}}) => <Heading {{...props}} {''.join(Tag(name='', props=Style({'as_': 'h1'})).format_props())} />}}"
},
'{{"h1": ({node, ...props}) => <Heading {...props} as={`h1`} />}}',
),
],
)
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( @pytest.mark.parametrize(
"props,test_props", "props,test_props",
[ [

View File

@ -9,6 +9,9 @@ from packaging import version
from reflex import constants from reflex import constants
from reflex.base import Base 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 ( from reflex.utils import (
build, build,
format, format,
@ -17,7 +20,11 @@ from reflex.utils import (
types, types,
) )
from reflex.utils import exec as utils_exec 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(): def get_above_max_version():
@ -262,6 +269,79 @@ def test_format_route(route: str, expected: bool):
assert format.format_route(route) == expected 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}}) => <Heading {{...props}} {''.join(Tag(name='', props=Style({'as_': 'h1'})).format_props())} />}}"
},
'{{"h1": ({node, ...props}) => <Heading {...props} as={`h1`} />}}',
),
],
)
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): def test_validate_invalid_bun_path(mocker):
"""Test that an error is thrown when a custom specified bun path is not valid """Test that an error is thrown when a custom specified bun path is not valid
or does not exist. or does not exist.