Răsfoiți Sursa

Support f-strings in component children and non-style props (#1575)

Nikhil Rao 1 an în urmă
părinte
comite
6d15326abf

+ 11 - 0
reflex/components/component.py

@@ -658,16 +658,25 @@ class CustomComponent(Component):
         # Set the props.
         # Set the props.
         props = typing.get_type_hints(self.component_fn)
         props = typing.get_type_hints(self.component_fn)
         for key, value in kwargs.items():
         for key, value in kwargs.items():
+            # Skip kwargs that are not props.
             if key not in props:
             if key not in props:
                 continue
                 continue
+
+            # Get the type based on the annotation.
             type_ = props[key]
             type_ = props[key]
+
+            # Handle event chains.
             if types._issubclass(type_, EventChain):
             if types._issubclass(type_, EventChain):
                 value = self._create_event_chain(key, value)
                 value = self._create_event_chain(key, value)
                 self.props[format.to_camel_case(key)] = value
                 self.props[format.to_camel_case(key)] = value
                 continue
                 continue
+
+            # Convert the type to a Var, then get the type of the var.
             if not types._issubclass(type_, Var):
             if not types._issubclass(type_, Var):
                 type_ = Var[type_]
                 type_ = Var[type_]
             type_ = types.get_args(type_)[0]
             type_ = types.get_args(type_)[0]
+
+            # Handle subclasses of Base.
             if types._issubclass(type_, Base):
             if types._issubclass(type_, Base):
                 try:
                 try:
                     value = BaseVar(name=value.json(), type_=type_, is_local=True)
                     value = BaseVar(name=value.json(), type_=type_, is_local=True)
@@ -675,6 +684,8 @@ class CustomComponent(Component):
                     value = Var.create(value)
                     value = Var.create(value)
             else:
             else:
                 value = Var.create(value, is_string=type(value) is str)
                 value = Var.create(value, is_string=type(value) is str)
+
+            # Set the prop.
             self.props[format.to_camel_case(key)] = value
             self.props[format.to_camel_case(key)] = value
 
 
     def __eq__(self, other: Any) -> bool:
     def __eq__(self, other: Any) -> bool:

+ 1 - 1
reflex/components/tags/tag.py

@@ -73,7 +73,7 @@ class Tag(Base):
                 if not prop.is_local or prop.is_string:
                 if not prop.is_local or prop.is_string:
                     return str(prop)
                     return str(prop)
                 if types._issubclass(prop.type_, str):
                 if types._issubclass(prop.type_, str):
-                    return format.json_dumps(prop.full_name)
+                    return format.format_string(prop.full_name)
                 prop = prop.full_name
                 prop = prop.full_name
 
 
             # Handle event props.
             # Handle event props.

+ 6 - 3
reflex/utils/format.py

@@ -227,9 +227,12 @@ def format_cond(
 
 
     # Format prop conds.
     # Format prop conds.
     if is_prop:
     if is_prop:
-        prop1 = Var.create(true_value, is_string=type(true_value) is str)
-        prop2 = Var.create(false_value, is_string=type(false_value) is str)
-        assert prop1 is not None and prop2 is not None, "Invalid prop values"
+        prop1 = Var.create_safe(true_value, is_string=type(true_value) is str).set(
+            is_local=True
+        )  # type: ignore
+        prop2 = Var.create_safe(false_value, is_string=type(false_value) is str).set(
+            is_local=True
+        )  # type: ignore
         return f"{cond} ? {prop1} : {prop2}".replace("{", "").replace("}", "")
         return f"{cond} ? {prop1} : {prop2}".replace("{", "").replace("}", "")
 
 
     # Format component conds.
     # Format component conds.

+ 22 - 2
reflex/vars.py

@@ -187,6 +187,19 @@ class Var(ABC):
             out = format.format_string(out)
             out = format.format_string(out)
         return out
         return out
 
 
+    def __format__(self, format_spec: str) -> str:
+        """Format the var into a Javascript equivalent to an f-string.
+
+        Args:
+            format_spec: The format specifier (Ignored for now).
+
+        Returns:
+            The formatted var.
+        """
+        if self.is_local:
+            return str(self)
+        return f"${str(self)}"
+
     def __getitem__(self, i: Any) -> Var:
     def __getitem__(self, i: Any) -> Var:
         """Index into a var.
         """Index into a var.
 
 
@@ -206,8 +219,8 @@ class Var(ABC):
         ):
         ):
             if self.type_ == Any:
             if self.type_ == Any:
                 raise TypeError(
                 raise TypeError(
-                    f"Could not index into var of type Any. (If you are trying to index into a state var, "
-                    f"add the correct type annotation to the var.)"
+                    "Could not index into var of type Any. (If you are trying to index into a state var, "
+                    "add the correct type annotation to the var.)"
                 )
                 )
             raise TypeError(
             raise TypeError(
                 f"Var {self.name} of type {self.type_} does not support indexing."
                 f"Var {self.name} of type {self.type_} does not support indexing."
@@ -241,6 +254,7 @@ class Var(ABC):
                     name=f"{self.name}.slice({start}, {stop})",
                     name=f"{self.name}.slice({start}, {stop})",
                     type_=self.type_,
                     type_=self.type_,
                     state=self.state,
                     state=self.state,
+                    is_local=self.is_local,
                 )
                 )
 
 
             # Get the type of the indexed var.
             # Get the type of the indexed var.
@@ -255,6 +269,7 @@ class Var(ABC):
                 name=f"{self.name}.at({i})",
                 name=f"{self.name}.at({i})",
                 type_=type_,
                 type_=type_,
                 state=self.state,
                 state=self.state,
+                is_local=self.is_local,
             )
             )
 
 
         # Dictionary / dataframe indexing.
         # Dictionary / dataframe indexing.
@@ -281,6 +296,7 @@ class Var(ABC):
             name=f"{self.name}[{i}]",
             name=f"{self.name}[{i}]",
             type_=type_,
             type_=type_,
             state=self.state,
             state=self.state,
+            is_local=self.is_local,
         )
         )
 
 
     def __getattribute__(self, name: str) -> Var:
     def __getattribute__(self, name: str) -> Var:
@@ -313,6 +329,7 @@ class Var(ABC):
                         name=f"{self.name}.{name}",
                         name=f"{self.name}.{name}",
                         type_=type_,
                         type_=type_,
                         state=self.state,
                         state=self.state,
+                        is_local=self.is_local,
                     )
                     )
             raise AttributeError(
             raise AttributeError(
                 f"The State var `{self.full_name}` has no attribute '{name}' or may have been annotated "
                 f"The State var `{self.full_name}` has no attribute '{name}' or may have been annotated "
@@ -359,6 +376,7 @@ class Var(ABC):
         return BaseVar(
         return BaseVar(
             name=name,
             name=name,
             type_=type_,
             type_=type_,
+            is_local=self.is_local,
         )
         )
 
 
     def compare(self, op: str, other: Var) -> Var:
     def compare(self, op: str, other: Var) -> Var:
@@ -411,6 +429,7 @@ class Var(ABC):
         return BaseVar(
         return BaseVar(
             name=f"{self.full_name}.length",
             name=f"{self.full_name}.length",
             type_=int,
             type_=int,
+            is_local=self.is_local,
         )
         )
 
 
     def __eq__(self, other: Var) -> Var:
     def __eq__(self, other: Var) -> Var:
@@ -682,6 +701,7 @@ class Var(ABC):
         return BaseVar(
         return BaseVar(
             name=f"{self.full_name}.map(({arg.name}, i) => {fn(arg, key='i')})",
             name=f"{self.full_name}.map(({arg.name}, i) => {fn(arg, key='i')})",
             type_=self.type_,
             type_=self.type_,
+            is_local=self.is_local,
         )
         )
 
 
     def to(self, type_: Type) -> Var:
     def to(self, type_: Type) -> Var:

+ 1 - 1
tests/components/base/test_script.py

@@ -22,7 +22,7 @@ def test_script_src():
     assert render_dict["name"] == "Script"
     assert render_dict["name"] == "Script"
     assert not render_dict["contents"]
     assert not render_dict["contents"]
     assert not render_dict["children"]
     assert not render_dict["children"]
-    assert 'src="foo.js"' in render_dict["props"]
+    assert "src={`foo.js`}" in render_dict["props"]
 
 
 
 
 def test_script_neither():
 def test_script_neither():

+ 1 - 1
tests/components/forms/test_uploads.py

@@ -68,7 +68,7 @@ def test_upload_component_render(upload_component):
     # input, button and text inside of box
     # input, button and text inside of box
     [input, button, text] = box["children"]
     [input, button, text] = box["children"]
     assert input["name"] == "Input"
     assert input["name"] == "Input"
-    assert input["props"] == ['type="file"', "{...getInputProps()}"]
+    assert input["props"] == ["type={`file`}", "{...getInputProps()}"]
 
 
     assert button["name"] == "Button"
     assert button["name"] == "Button"
     assert button["children"][0]["contents"] == "{`select file`}"
     assert button["children"][0]["contents"] == "{`select file`}"

+ 2 - 2
tests/components/test_tag.py

@@ -74,8 +74,8 @@ def test_format_prop(prop: Var, formatted: str):
     [
     [
         ({}, []),
         ({}, []),
         ({"key": 1}, ["key={1}"]),
         ({"key": 1}, ["key={1}"]),
-        ({"key": "value"}, ['key="value"']),
-        ({"key": True, "key2": "value2"}, ["key={true}", 'key2="value2"']),
+        ({"key": "value"}, ["key={`value`}"]),
+        ({"key": True, "key2": "value2"}, ["key={true}", "key2={`value2`}"]),
     ],
     ],
 )
 )
 def test_format_props(props: Dict[str, Var], test_props: List):
 def test_format_props(props: Dict[str, Var], test_props: List):

+ 19 - 1
tests/test_var.py

@@ -239,7 +239,7 @@ def test_create_type_error():
 
 
 
 
 def v(value) -> Var:
 def v(value) -> Var:
-    val = Var.create(value)
+    val = Var.create(value, is_local=False)
     assert val is not None
     assert val is not None
     return val
     return val
 
 
@@ -614,3 +614,21 @@ def test_get_local_storage_raise_error(key):
         err.value.args[0]
         err.value.args[0]
         == f"Local storage keys can only be of type `str` or `var` of type `str`. Got `{type_}` instead."
         == f"Local storage keys can only be of type `str` or `var` of type `str`. Got `{type_}` instead."
     )
     )
+
+
+@pytest.mark.parametrize(
+    "out, expected",
+    [
+        (f"{BaseVar(name='var', type_=str)}", "${var}"),
+        (
+            f"testing f-string with {BaseVar(name='myvar', state='state', type_=int)}",
+            "testing f-string with ${state.myvar}",
+        ),
+        (
+            f"testing local f-string {BaseVar(name='x', is_local=True, type_=str)}",
+            "testing local f-string x",
+        ),
+    ],
+)
+def test_fstrings(out, expected):
+    assert out == expected