Support f-strings in component children and non-style props (#1575)
This commit is contained in:
parent
e61dd5e5b6
commit
6d15326abf
@ -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:
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
@ -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():
|
||||||
|
@ -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`}"
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user