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

This commit is contained in:
Nikhil Rao 2023-08-14 11:33:16 -07:00 committed by GitHub
parent e61dd5e5b6
commit 6d15326abf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 63 additions and 11 deletions

View File

@ -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:

View File

@ -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.

View File

@ -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) prop1 = Var.create_safe(true_value, is_string=type(true_value) is str).set(
prop2 = Var.create(false_value, is_string=type(false_value) is str) is_local=True
assert prop1 is not None and prop2 is not None, "Invalid prop values" ) # 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.

View File

@ -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, " "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.)" "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:

View File

@ -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():

View File

@ -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`}"

View File

@ -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": "value"}, ["key={`value`}"]),
({"key": True, "key2": "value2"}, ["key={true}", 'key2="value2"']), ({"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):

View File

@ -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