From 6d15326abfc5f35a9d5171f0106fe9e650e3007d Mon Sep 17 00:00:00 2001
From: Nikhil Rao <nikhil@pynecone.io>
Date: Mon, 14 Aug 2023 11:33:16 -0700
Subject: [PATCH] Support f-strings in component children and non-style props
 (#1575)

---
 reflex/components/component.py         | 11 +++++++++++
 reflex/components/tags/tag.py          |  2 +-
 reflex/utils/format.py                 |  9 ++++++---
 reflex/vars.py                         | 24 ++++++++++++++++++++++--
 tests/components/base/test_script.py   |  2 +-
 tests/components/forms/test_uploads.py |  2 +-
 tests/components/test_tag.py           |  4 ++--
 tests/test_var.py                      | 20 +++++++++++++++++++-
 8 files changed, 63 insertions(+), 11 deletions(-)

diff --git a/reflex/components/component.py b/reflex/components/component.py
index 80ce31e4b..841de73d6 100644
--- a/reflex/components/component.py
+++ b/reflex/components/component.py
@@ -658,16 +658,25 @@ class CustomComponent(Component):
         # Set the props.
         props = typing.get_type_hints(self.component_fn)
         for key, value in kwargs.items():
+            # Skip kwargs that are not props.
             if key not in props:
                 continue
+
+            # Get the type based on the annotation.
             type_ = props[key]
+
+            # Handle event chains.
             if types._issubclass(type_, EventChain):
                 value = self._create_event_chain(key, value)
                 self.props[format.to_camel_case(key)] = value
                 continue
+
+            # Convert the type to a Var, then get the type of the var.
             if not types._issubclass(type_, Var):
                 type_ = Var[type_]
             type_ = types.get_args(type_)[0]
+
+            # Handle subclasses of Base.
             if types._issubclass(type_, Base):
                 try:
                     value = BaseVar(name=value.json(), type_=type_, is_local=True)
@@ -675,6 +684,8 @@ class CustomComponent(Component):
                     value = Var.create(value)
             else:
                 value = Var.create(value, is_string=type(value) is str)
+
+            # Set the prop.
             self.props[format.to_camel_case(key)] = value
 
     def __eq__(self, other: Any) -> bool:
diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py
index 85525d118..b41936685 100644
--- a/reflex/components/tags/tag.py
+++ b/reflex/components/tags/tag.py
@@ -73,7 +73,7 @@ class Tag(Base):
                 if not prop.is_local or prop.is_string:
                     return str(prop)
                 if types._issubclass(prop.type_, str):
-                    return format.json_dumps(prop.full_name)
+                    return format.format_string(prop.full_name)
                 prop = prop.full_name
 
             # Handle event props.
diff --git a/reflex/utils/format.py b/reflex/utils/format.py
index 25e5ec4e7..0c92dc738 100644
--- a/reflex/utils/format.py
+++ b/reflex/utils/format.py
@@ -227,9 +227,12 @@ def format_cond(
 
     # Format prop conds.
     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("}", "")
 
     # Format component conds.
diff --git a/reflex/vars.py b/reflex/vars.py
index a4d57b479..fb2d19b46 100644
--- a/reflex/vars.py
+++ b/reflex/vars.py
@@ -187,6 +187,19 @@ class Var(ABC):
             out = format.format_string(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:
         """Index into a var.
 
@@ -206,8 +219,8 @@ class Var(ABC):
         ):
             if self.type_ == Any:
                 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(
                 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})",
                     type_=self.type_,
                     state=self.state,
+                    is_local=self.is_local,
                 )
 
             # Get the type of the indexed var.
@@ -255,6 +269,7 @@ class Var(ABC):
                 name=f"{self.name}.at({i})",
                 type_=type_,
                 state=self.state,
+                is_local=self.is_local,
             )
 
         # Dictionary / dataframe indexing.
@@ -281,6 +296,7 @@ class Var(ABC):
             name=f"{self.name}[{i}]",
             type_=type_,
             state=self.state,
+            is_local=self.is_local,
         )
 
     def __getattribute__(self, name: str) -> Var:
@@ -313,6 +329,7 @@ class Var(ABC):
                         name=f"{self.name}.{name}",
                         type_=type_,
                         state=self.state,
+                        is_local=self.is_local,
                     )
             raise AttributeError(
                 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(
             name=name,
             type_=type_,
+            is_local=self.is_local,
         )
 
     def compare(self, op: str, other: Var) -> Var:
@@ -411,6 +429,7 @@ class Var(ABC):
         return BaseVar(
             name=f"{self.full_name}.length",
             type_=int,
+            is_local=self.is_local,
         )
 
     def __eq__(self, other: Var) -> Var:
@@ -682,6 +701,7 @@ class Var(ABC):
         return BaseVar(
             name=f"{self.full_name}.map(({arg.name}, i) => {fn(arg, key='i')})",
             type_=self.type_,
+            is_local=self.is_local,
         )
 
     def to(self, type_: Type) -> Var:
diff --git a/tests/components/base/test_script.py b/tests/components/base/test_script.py
index 3f136c1e8..99cd985ac 100644
--- a/tests/components/base/test_script.py
+++ b/tests/components/base/test_script.py
@@ -22,7 +22,7 @@ def test_script_src():
     assert render_dict["name"] == "Script"
     assert not render_dict["contents"]
     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():
diff --git a/tests/components/forms/test_uploads.py b/tests/components/forms/test_uploads.py
index e18445a45..74134276f 100644
--- a/tests/components/forms/test_uploads.py
+++ b/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, text] = box["children"]
     assert input["name"] == "Input"
-    assert input["props"] == ['type="file"', "{...getInputProps()}"]
+    assert input["props"] == ["type={`file`}", "{...getInputProps()}"]
 
     assert button["name"] == "Button"
     assert button["children"][0]["contents"] == "{`select file`}"
diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py
index 5642ed723..9f71fd55a 100644
--- a/tests/components/test_tag.py
+++ b/tests/components/test_tag.py
@@ -74,8 +74,8 @@ def test_format_prop(prop: Var, formatted: str):
     [
         ({}, []),
         ({"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):
diff --git a/tests/test_var.py b/tests/test_var.py
index beb661f3c..f97c928d1 100644
--- a/tests/test_var.py
+++ b/tests/test_var.py
@@ -239,7 +239,7 @@ def test_create_type_error():
 
 
 def v(value) -> Var:
-    val = Var.create(value)
+    val = Var.create(value, is_local=False)
     assert val is not None
     return val
 
@@ -614,3 +614,21 @@ def test_get_local_storage_raise_error(key):
         err.value.args[0]
         == 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