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