Var field cleanup (#1943)

This commit is contained in:
Masen Furer 2023-10-13 14:53:55 -07:00 committed by GitHub
parent 684912e33b
commit 024cb5fa9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 691 additions and 530 deletions

View File

@ -131,8 +131,8 @@ def EventChain():
@app.add_page
def index():
return rx.fragment(
rx.input(value=State.token, readonly=True, id="token"),
rx.input(value=State.interim_value, readonly=True, id="interim_value"),
rx.input(value=State.token, is_read_only=True, id="token"),
rx.input(value=State.interim_value, is_read_only=True, id="interim_value"),
rx.button(
"Return Event",
id="return_event",

View File

@ -65,13 +65,13 @@ class Base(pydantic.BaseModel):
default_value: The default value of the field
"""
new_field = ModelField.infer(
name=var.name,
name=var._var_name,
value=default_value,
annotation=var.type_,
annotation=var._var_type,
class_validators=None,
config=cls.__config__,
)
cls.__fields__.update({var.name: new_field})
cls.__fields__.update({var._var_name: new_field})
def get_value(self, key: str) -> Any:
"""Get the value of a field.

View File

@ -243,7 +243,7 @@ def compile_custom_component(
}
# Concatenate the props.
props = [prop.name for prop in component.get_prop_vars()]
props = [prop._var_name for prop in component.get_prop_vars()]
# Compile the component.
return (

View File

@ -94,4 +94,4 @@ def client_side(javascript_code) -> Var[EventChain]:
deprecation_version="0.2.9",
removal_version="0.3.0",
)
return BaseVar(name=f"...args => {{{javascript_code}}}", type_=EventChain)
return BaseVar(_var_name=f"...args => {{{javascript_code}}}", _var_type=EventChain)

View File

@ -155,7 +155,7 @@ class Component(Base, ABC):
raise TypeError
# Get the passed type and the var type.
passed_type = kwargs[key].type_
passed_type = kwargs[key]._var_type
expected_type = fields[key].outer_type_.__args__[0]
except TypeError:
# If it is not a valid var, check the base types.
@ -222,7 +222,7 @@ class Component(Base, ABC):
# If it's an event chain var, return it.
if isinstance(value, Var):
if value.type_ is not EventChain:
if value._var_type is not EventChain:
raise ValueError(f"Invalid event chain: {value}")
return value
@ -388,7 +388,7 @@ class Component(Base, ABC):
# Add ref to element if `id` is not None.
ref = self.get_ref()
if ref is not None:
props["ref"] = Var.create(ref, is_local=False)
props["ref"] = Var.create(ref, _var_is_local=False)
return tag.add_props(**props)
@ -440,7 +440,7 @@ class Component(Base, ABC):
children = [
child
if isinstance(child, Component)
else Bare.create(contents=Var.create(child, is_string=True))
else Bare.create(contents=Var.create(child, _var_is_string=True))
for child in children
]
@ -808,11 +808,13 @@ class CustomComponent(Component):
# Handle subclasses of Base.
if types._issubclass(type_, Base):
try:
value = BaseVar(name=value.json(), type_=type_, is_local=True)
value = BaseVar(
_var_name=value.json(), _var_type=type_, _var_is_local=True
)
except Exception:
value = Var.create(value)
else:
value = Var.create(value, is_string=type(value) is str)
value = Var.create(value, _var_is_string=type(value) is str)
# Set the prop.
self.props[format.to_camel_case(key)] = value
@ -888,8 +890,10 @@ class CustomComponent(Component):
"""
return [
BaseVar(
name=name,
type_=prop.type_ if types._isinstance(prop, Var) else type(prop),
_var_name=name,
_var_type=prop._var_type
if types._isinstance(prop, Var)
else type(prop),
)
for name, prop in self.props.items()
]

View File

@ -48,7 +48,8 @@ class CodeBlock(Component):
merged_imports = super()._get_imports()
if self.theme is not None:
merged_imports = imports.merge_imports(
merged_imports, {PRISM_STYLES_PATH: {ImportVar(tag=self.theme.name)}}
merged_imports,
{PRISM_STYLES_PATH: {ImportVar(tag=self.theme._var_name)}},
)
return merged_imports
@ -113,7 +114,7 @@ class CodeBlock(Component):
out = super()._render()
if self.theme is not None:
out.add_props(
style=Var.create(self.theme.name, is_local=False)
style=Var.create(self.theme._var_name, _var_is_local=False)
).remove_props("theme")
return out

View File

@ -64,7 +64,7 @@ class DataTable(Gridjs):
# The annotation should be provided if data is a computed var. We need this to know how to
# render pandas dataframes.
if isinstance(data, ComputedVar) and data.type_ == Any:
if isinstance(data, ComputedVar) and data._var_type == Any:
raise ValueError(
"Annotation of the computed var assigned to the data field should be provided."
)
@ -72,7 +72,7 @@ class DataTable(Gridjs):
if (
columns is not None
and isinstance(columns, ComputedVar)
and columns.type_ == Any
and columns._var_type == Any
):
raise ValueError(
"Annotation of the computed var assigned to the column field should be provided."
@ -81,7 +81,7 @@ class DataTable(Gridjs):
# If data is a pandas dataframe and columns are provided throw an error.
if (
types.is_dataframe(type(data))
or (isinstance(data, Var) and types.is_dataframe(data.type_))
or (isinstance(data, Var) and types.is_dataframe(data._var_type))
) and columns is not None:
raise ValueError(
"Cannot pass in both a pandas dataframe and columns to the data_table component."
@ -89,7 +89,7 @@ class DataTable(Gridjs):
# If data is a list and columns are not provided, throw an error
if (
(isinstance(data, Var) and types._issubclass(data.type_, List))
(isinstance(data, Var) and types._issubclass(data._var_type, List))
or issubclass(type(data), List)
) and columns is None:
raise ValueError(
@ -109,16 +109,16 @@ class DataTable(Gridjs):
)
def _render(self) -> Tag:
if isinstance(self.data, Var) and types.is_dataframe(self.data.type_):
if isinstance(self.data, Var) and types.is_dataframe(self.data._var_type):
self.columns = BaseVar(
name=f"{self.data.name}.columns",
type_=List[Any],
state=self.data.state,
_var_name=f"{self.data._var_name}.columns",
_var_type=List[Any],
_var_state=self.data._var_state,
)
self.data = BaseVar(
name=f"{self.data.name}.data",
type_=List[List[Any]],
state=self.data.state,
_var_name=f"{self.data._var_name}.data",
_var_type=List[List[Any]],
_var_state=self.data._var_state,
)
if types.is_dataframe(type(self.data)):
# If given a pandas df break up the data and columns

View File

@ -101,7 +101,9 @@ class Thead(ChakraComponent):
if (
(
isinstance(headers, Var)
and not types.check_type_in_allowed_types(headers.type_, allowed_types)
and not types.check_type_in_allowed_types(
headers._var_type, allowed_types
)
)
or not isinstance(headers, Var)
and not types.check_type_in_allowed_types(type(headers), allowed_types)
@ -156,7 +158,7 @@ class Tbody(ChakraComponent):
"""
allowed_subclasses = (List, Tuple)
if isinstance(rows, Var):
outer_type = rows.type_
outer_type = rows._var_type
inner_type = (
outer_type.__args__[0] if hasattr(outer_type, "__args__") else None
)
@ -222,7 +224,9 @@ class Tfoot(ChakraComponent):
if (
(
isinstance(footers, Var)
and not types.check_type_in_allowed_types(footers.type_, allowed_types)
and not types.check_type_in_allowed_types(
footers._var_type, allowed_types
)
)
or not isinstance(footers, Var)
and not types.check_type_in_allowed_types(type(footers), allowed_types)

View File

@ -58,7 +58,7 @@ class DebounceInput(Component):
raise ValueError("DebounceInput child requires an on_change handler")
child_ref = child.get_ref()
if child_ref and not props.get("ref"):
props["input_ref"] = Var.create(child_ref, is_local=False)
props["input_ref"] = Var.create(child_ref, _var_is_local=False)
self.children = []
tag = super()._render()
tag.add_props(
@ -67,7 +67,9 @@ class DebounceInput(Component):
sx=child.style,
id=child.id,
class_name=child.class_name,
element=Var.create("{%s}" % child.tag, is_local=False, is_string=False),
element=Var.create(
"{%s}" % child.tag, _var_is_local=False, _var_is_string=False
),
)
# do NOT render the child, DebounceInput will create it
object.__setattr__(child, "render", lambda: "")

View File

@ -29,10 +29,12 @@ class Form(ChakraComponent):
# to collect data
if ref.startswith("refs_"):
form_refs[ref[5:-3]] = Var.create(
f"getRefValues({ref[:-3]})", is_local=False
f"getRefValues({ref[:-3]})", _var_is_local=False
)
else:
form_refs[ref[4:]] = Var.create(f"getRefValue({ref})", is_local=False)
form_refs[ref[4:]] = Var.create(
f"getRefValue({ref})", _var_is_local=False
)
return {
**super().get_event_triggers(),

View File

@ -308,7 +308,9 @@ class Select(Component):
return {
**super().get_event_triggers(),
EventTriggers.ON_CHANGE: (
lambda e0: [Var.create_safe(f"{e0}.map(e => e.value)", is_local=True)]
lambda e0: [
Var.create_safe(f"{e0}.map(e => e.value)", _var_is_local=True)
]
if self.is_multi
else lambda e0: [e0]
),

View File

@ -11,13 +11,17 @@ from reflex.event import EventChain
from reflex.vars import BaseVar, Var
files_state: str = "const [files, setFiles] = useState([]);"
upload_file: BaseVar = BaseVar(name="e => setFiles((files) => e)", type_=EventChain)
upload_file: BaseVar = BaseVar(
_var_name="e => setFiles((files) => e)", _var_type=EventChain
)
# Use this var along with the Upload component to render the list of selected files.
selected_files: BaseVar = BaseVar(name="files.map((f) => f.name)", type_=List[str])
selected_files: BaseVar = BaseVar(
_var_name="files.map((f) => f.name)", _var_type=List[str]
)
clear_selected_files: BaseVar = BaseVar(
name="_e => setFiles((files) => [])", type_=EventChain
_var_name="_e => setFiles((files) => [])", _var_type=EventChain
)
@ -77,7 +81,9 @@ class Upload(Component):
}
# The file input to use.
upload = Input.create(type_="file")
upload.special_props = {BaseVar(name="{...getInputProps()}", type_=None)}
upload.special_props = {
BaseVar(_var_name="{...getInputProps()}", _var_type=None)
}
# The dropzone to use.
zone = Box.create(
@ -85,7 +91,7 @@ class Upload(Component):
*children,
**{k: v for k, v in props.items() if k not in supported_props},
)
zone.special_props = {BaseVar(name="{...getRootProps()}", type_=None)}
zone.special_props = {BaseVar(_var_name="{...getRootProps()}", _var_type=None)}
# Create the component.
return super().create(zone, on_drop=upload_file, **upload_props)

View File

@ -77,7 +77,7 @@ class Cond(Component):
).set(
props=tag.format_props(),
),
cond_state=f"isTrue({self.cond.full_name})",
cond_state=f"isTrue({self.cond._var_full_name})",
)
@ -118,11 +118,11 @@ def cond(condition: Any, c1: Any, c2: Any = None):
# Create the conditional var.
return BaseVar(
name=format.format_cond(
cond=cond_var.full_name,
_var_name=format.format_cond(
cond=cond_var._var_full_name,
true_value=c1,
false_value=c2,
is_prop=True,
),
type_=c1.type_ if isinstance(c1, BaseVar) else type(c1),
_var_type=c1._var_type if isinstance(c1, BaseVar) else type(c1),
)

View File

@ -35,18 +35,18 @@ class Foreach(Component):
"""
try:
type_ = (
iterable.type_
if iterable.type_.mro()[0] == dict
else iterable.type_.__args__[0]
iterable._var_type
if iterable._var_type.mro()[0] == dict
else iterable._var_type.__args__[0]
)
except Exception:
type_ = Any
iterable = Var.create(iterable) # type: ignore
if iterable.type_ == Any:
if iterable._var_type == Any:
raise TypeError(
f"Could not foreach over var of type Any. (If you are trying to foreach over a state var, add a type annotation to the var.)"
)
arg = BaseVar(name="_", type_=type_, is_local=True)
arg = BaseVar(_var_name="_", _var_type=type_, _var_is_local=True)
return cls(
iterable=iterable,
render_fn=render_fn,
@ -66,15 +66,15 @@ class Foreach(Component):
tag = self._render()
try:
type_ = (
self.iterable.type_
if self.iterable.type_.mro()[0] == dict
else self.iterable.type_.__args__[0]
self.iterable._var_type
if self.iterable._var_type.mro()[0] == dict
else self.iterable._var_type.__args__[0]
)
except Exception:
type_ = Any
arg = BaseVar(
name=get_unique_variable_name(),
type_=type_,
_var_name=get_unique_variable_name(),
_var_type=type_,
)
index_arg = tag.get_index_var_arg()
component = tag.render_component(self.render_fn, arg)
@ -89,8 +89,8 @@ class Foreach(Component):
children=[component.render()],
props=tag.format_props(),
),
iterable_state=tag.iterable.full_name,
arg_name=arg.name,
iterable_state=tag.iterable._var_full_name,
arg_name=arg._var_name,
arg_index=index_arg,
iterable_type=tag.iterable.type_.mro()[0].__name__,
iterable_type=tag.iterable._var_type.mro()[0].__name__,
)

View File

@ -77,7 +77,7 @@ class Image(ChakraComponent):
"""
src = props.get("src", None)
if src is not None and not isinstance(src, (Var)):
props["src"] = Var.create(value=src, is_string=True)
props["src"] = Var.create(value=src, _var_is_string=True)
return super().create(*children, **props)

View File

@ -23,7 +23,7 @@ class Link(ChakraComponent):
text: Var[str]
# What the link renders to.
as_: Var[str] = BaseVar.create("{NextLink}", is_local=False) # type: ignore
as_: Var[str] = BaseVar.create(value="{NextLink}", _var_is_local=False) # type: ignore
# If true, the link will open in new tab.
is_external: Var[bool]

View File

@ -13,14 +13,14 @@ from reflex.vars import ImportVar, Var
connection_error: Var = Var.create_safe(
value="(connectError !== null) ? connectError.message : ''",
is_local=False,
is_string=False,
_var_is_local=False,
_var_is_string=False,
)
has_connection_error: Var = Var.create_safe(
value="connectError !== null",
is_string=False,
_var_is_string=False,
)
has_connection_error.type_ = bool
has_connection_error._var_type = bool
class WebsocketTargetURL(Bare):

View File

@ -31,8 +31,8 @@ class IterTag(Tag):
The index var.
"""
return BaseVar(
name=INDEX_VAR,
type_=int,
_var_name=INDEX_VAR,
_var_type=int,
)
@staticmethod
@ -43,9 +43,9 @@ class IterTag(Tag):
The index var.
"""
return BaseVar(
name=INDEX_VAR,
type_=int,
is_local=True,
_var_name=INDEX_VAR,
_var_type=int,
_var_is_local=True,
)
@staticmethod

View File

@ -17,18 +17,18 @@ from reflex.utils import console, imports, types
from reflex.vars import ImportVar, Var
# Special vars used in the component map.
_CHILDREN = Var.create_safe("children", is_local=False)
_PROPS = Var.create_safe("...props", is_local=False)
_CHILDREN = Var.create_safe("children", _var_is_local=False)
_PROPS = Var.create_safe("...props", _var_is_local=False)
_MOCK_ARG = Var.create_safe("")
# Special remark plugins.
_REMARK_MATH = Var.create_safe("remarkMath", is_local=False)
_REMARK_GFM = Var.create_safe("remarkGfm", is_local=False)
_REMARK_MATH = Var.create_safe("remarkMath", _var_is_local=False)
_REMARK_GFM = Var.create_safe("remarkGfm", _var_is_local=False)
_REMARK_PLUGINS = Var.create_safe([_REMARK_MATH, _REMARK_GFM])
# Special rehype plugins.
_REHYPE_KATEX = Var.create_safe("rehypeKatex", is_local=False)
_REHYPE_RAW = Var.create_safe("rehypeRaw", is_local=False)
_REHYPE_KATEX = Var.create_safe("rehypeKatex", _var_is_local=False)
_REHYPE_RAW = Var.create_safe("rehypeRaw", _var_is_local=False)
_REHYPE_PLUGINS = Var.create_safe([_REHYPE_KATEX, _REHYPE_RAW])
# Component Mapping
@ -153,13 +153,17 @@ class Markdown(Component):
{
"": {ImportVar(tag="katex/dist/katex.min.css")},
"remark-math@5.1.1": {
ImportVar(tag=_REMARK_MATH.name, is_default=True)
ImportVar(tag=_REMARK_MATH._var_name, is_default=True)
},
"remark-gfm@3.0.1": {
ImportVar(tag=_REMARK_GFM._var_name, is_default=True)
},
"remark-gfm@3.0.1": {ImportVar(tag=_REMARK_GFM.name, is_default=True)},
"rehype-katex@6.0.3": {
ImportVar(tag=_REHYPE_KATEX.name, is_default=True)
ImportVar(tag=_REHYPE_KATEX._var_name, is_default=True)
},
"rehype-raw@6.1.1": {
ImportVar(tag=_REHYPE_RAW._var_name, is_default=True)
},
"rehype-raw@6.1.1": {ImportVar(tag=_REHYPE_RAW.name, is_default=True)},
}
)
@ -226,20 +230,20 @@ class Markdown(Component):
The formatted component map.
"""
components = {
tag: f"{{({{{_CHILDREN.name}, {_PROPS.name}}}) => {self.format_component(tag)}}}"
tag: f"{{({{{_CHILDREN._var_name}, {_PROPS._var_name}}}) => {self.format_component(tag)}}}"
for tag in self.component_map
}
# Separate out inline code and code blocks.
components[
"code"
] = f"""{{({{inline, className, {_CHILDREN.name}, {_PROPS.name}}}) => {{
] = f"""{{({{inline, className, {_CHILDREN._var_name}, {_PROPS._var_name}}}) => {{
const match = (className || '').match(/language-(?<lang>.*)/);
const language = match ? match[1] : '';
return inline ? (
{self.format_component("code")}
) : (
{self.format_component("codeblock", language=Var.create_safe("language", is_local=False), children=Var.create_safe("String(children)", is_local=False))}
{self.format_component("codeblock", language=Var.create_safe("language", _var_is_local=False), children=Var.create_safe("String(children)", _var_is_local=False))}
);
}}}}""".replace(
"\n", " "

View File

@ -153,7 +153,7 @@ class EventHandler(Base):
# Otherwise, convert to JSON.
try:
values.append(Var.create(arg, is_string=type(arg) is str))
values.append(Var.create(arg, _var_is_string=type(arg) is str))
except TypeError as e:
raise TypeError(
f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
@ -211,7 +211,7 @@ class FrontendEvent(Base):
# The default event argument.
EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
EVENT_ARG = BaseVar(_var_name="_e", _var_type=FrontendEvent, _var_is_local=True)
class FileUpload(Base):
@ -241,7 +241,7 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
return EventSpec(
handler=EventHandler(fn=fn),
args=tuple(
(Var.create_safe(k), Var.create_safe(v, is_string=type(v) is str))
(Var.create_safe(k), Var.create_safe(v, _var_is_string=type(v) is str))
for k, v in kwargs.items()
),
)
@ -560,9 +560,9 @@ def parse_args_spec(arg_spec: ArgsSpec):
return arg_spec(
*[
BaseVar(
name=f"_{l_arg}",
type_=spec.annotations.get(l_arg, FrontendEvent),
is_local=True,
_var_name=f"_{l_arg}",
_var_type=spec.annotations.get(l_arg, FrontendEvent),
_var_is_local=True,
)
for l_arg in spec.args
]
@ -675,7 +675,7 @@ def fix_events(
e = e()
assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
name = format.format_event_handler(e.handler)
payload = {k.name: v._decode() for k, v in e.args} # type: ignore
payload = {k._var_name: v._decode() for k, v in e.args} # type: ignore
# Create an event and append it to the list.
out.append(

View File

@ -132,7 +132,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
)
for cvar_name, cvar in self.computed_vars.items():
# Add the dependencies.
for var in cvar.deps(objclass=type(self)):
for var in cvar._deps(objclass=type(self)):
self.computed_var_dependencies[var].add(cvar_name)
if var in inherited_vars:
# track that this substate depends on its parent for this var
@ -211,12 +211,14 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
# Set the base and computed vars.
cls.base_vars = {
f.name: BaseVar(name=f.name, type_=f.outer_type_).set_state(cls)
f.name: BaseVar(_var_name=f.name, _var_type=f.outer_type_)._var_set_state(
cls
)
for f in cls.get_fields().values()
if f.name not in cls.get_skip_vars()
}
cls.computed_vars = {
v.name: v.set_state(cls)
v._var_name: v._var_set_state(cls)
for v in cls.__dict__.values()
if isinstance(v, ComputedVar)
}
@ -389,12 +391,12 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
Raises:
TypeError: if the variable has an incorrect type
"""
if not types.is_valid_var_type(prop.type_):
if not types.is_valid_var_type(prop._var_type):
raise TypeError(
"State vars must be primitive Python types, "
"Plotly figures, Pandas dataframes, "
"or subclasses of rx.Base. "
f'Found var "{prop.name}" with type {prop.type_}.'
f'Found var "{prop._var_name}" with type {prop._var_type}.'
)
cls._set_var(prop)
cls._create_setter(prop)
@ -421,8 +423,8 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
)
# create the variable based on name and type
var = BaseVar(name=name, type_=type_)
var.set_state(cls)
var = BaseVar(_var_name=name, _var_type=type_)
var._var_set_state(cls)
# add the pydantic field dynamically (must be done before _init_var)
cls.add_field(var, default_value)
@ -444,7 +446,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
Args:
prop: The var instance to set.
"""
setattr(cls, prop.name, prop)
setattr(cls, prop._var_name, prop)
@classmethod
def _create_setter(cls, prop: BaseVar):
@ -467,7 +469,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
prop: The var to set the default value for.
"""
# Get the pydantic field for the var.
field = cls.get_fields()[prop.name]
field = cls.get_fields()[prop._var_name]
default_value = prop.get_default_value()
if field.required and default_value is not None:
field.required = False
@ -599,8 +601,9 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
func = arglist_factory(param)
else:
continue
func.fget.__name__ = param # to allow passing as a prop # type: ignore
cls.vars[param] = cls.computed_vars[param] = func.set_state(cls) # type: ignore
# to allow passing as a prop
func._var_name = param
cls.vars[param] = cls.computed_vars[param] = func._var_set_state(cls) # type: ignore
setattr(cls, param, func)
def __getattribute__(self, name: str) -> Any:
@ -912,7 +915,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
return set(
cvar_name
for cvar_name, cvar in self.computed_vars.items()
if not cvar.cache
if not cvar._cache
)
def _mark_dirty_computed_vars(self) -> None:

View File

@ -7,8 +7,8 @@ from reflex.event import EventChain
from reflex.utils import format
from reflex.vars import BaseVar, Var
color_mode = BaseVar(name=constants.ColorMode.NAME, type_="str")
toggle_color_mode = BaseVar(name=constants.ColorMode.TOGGLE, type_=EventChain)
color_mode = BaseVar(_var_name=constants.ColorMode.NAME, _var_type="str")
toggle_color_mode = BaseVar(_var_name=constants.ColorMode.TOGGLE, _var_type=EventChain)
def convert(style_dict):

View File

@ -106,6 +106,7 @@ class AppHarness:
app_instance: Optional[reflex.App] = None
frontend_process: Optional[subprocess.Popen] = None
frontend_url: Optional[str] = None
frontend_output_thread: Optional[threading.Thread] = None
backend_thread: Optional[threading.Thread] = None
backend: Optional[uvicorn.Server] = None
state_manager: Optional[StateManagerMemory | StateManagerRedis] = None
@ -230,6 +231,18 @@ class AppHarness:
if self.frontend_url is None:
raise RuntimeError("Frontend did not start")
def consume_frontend_output():
while True:
line = (
self.frontend_process.stdout.readline() # pyright: ignore [reportOptionalMemberAccess]
)
if not line:
break
print(line)
self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
self.frontend_output_thread.start()
def start(self) -> "AppHarness":
"""Start the backend in a new thread and dev frontend as a separate process.
@ -278,6 +291,8 @@ class AppHarness:
self.frontend_process.communicate()
if self.backend_thread is not None:
self.backend_thread.join()
if self.frontend_output_thread is not None:
self.frontend_output_thread.join()
for driver in self._frontends:
driver.quit()

View File

@ -203,13 +203,13 @@ def format_var(var: Var) -> str:
Returns:
The formatted Var.
"""
if not var.is_local or var.is_string:
if not var._var_is_local or var._var_is_string:
return str(var)
if types._issubclass(var.type_, str):
return format_string(var.full_name)
if is_wrapped(var.full_name, "{"):
return var.full_name
return json_dumps(var.full_name)
if types._issubclass(var._var_type, str):
return format_string(var._var_full_name)
if is_wrapped(var._var_full_name, "{"):
return var._var_full_name
return json_dumps(var._var_full_name)
def format_route(route: str, format_case=True) -> str:
@ -259,12 +259,16 @@ def format_cond(
# Format prop conds.
if is_prop:
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
prop1 = Var.create_safe(
true_value,
_var_is_string=type(true_value) is str,
)
prop1._var_is_local = True
prop2 = Var.create_safe(
false_value,
_var_is_string=type(false_value) is str,
)
prop2._var_is_local = True
return f"{cond} ? {prop1} : {prop2}".replace("{", "").replace("}", "")
# Format component conds.
@ -292,11 +296,11 @@ def format_prop(
try:
# Handle var props.
if isinstance(prop, Var):
if not prop.is_local or prop.is_string:
if not prop._var_is_local or prop._var_is_string:
return str(prop)
if types._issubclass(prop.type_, str):
return format_string(prop.full_name)
prop = prop.full_name
if types._issubclass(prop._var_type, str):
return format_string(prop._var_full_name)
prop = prop._var_full_name
# Handle event props.
elif isinstance(prop, EventChain):
@ -414,7 +418,12 @@ def format_event(event_spec: EventSpec) -> str:
args = ",".join(
[
":".join(
(name.name, json.dumps(val.name) if val.is_string else val.full_name)
(
name._var_name,
json.dumps(val._var_name)
if val._var_is_string
else val._var_full_name,
)
)
for name, val in event_spec.args
]
@ -448,7 +457,7 @@ def format_event_chain(
if isinstance(event_chain, Var):
from reflex.event import EventChain
if event_chain.type_ is not EventChain:
if event_chain._var_type is not EventChain:
raise ValueError(f"Invalid event chain: {event_chain}")
return "".join(
[
@ -540,7 +549,7 @@ def format_array_ref(refs: str, idx: Var | None) -> str:
"""
clean_ref = re.sub(r"[^\w]+", "_", refs)
if idx is not None:
idx.is_local = True
idx._var_is_local = True
return f"refs_{clean_ref}[{idx}]"
return f"refs_{clean_ref}"

View File

@ -2,11 +2,12 @@
from __future__ import annotations
import contextlib
import dataclasses
import dis
import json
import random
import string
from abc import ABC
import sys
from types import CodeType, FunctionType
from typing import (
TYPE_CHECKING,
@ -64,6 +65,18 @@ OPERATION_MAPPING = {
(list, list): {"+", ">", "<", "<=", ">="},
}
# These names were changed in reflex 0.3.0
REPLACED_NAMES = {
"full_name": "_var_full_name",
"name": "_var_name",
"state": "_var_state",
"type_": "_var_type",
"is_local": "_var_is_local",
"is_string": "_var_is_string",
"set_state": "_var_set_state",
"deps": "_deps",
}
def get_unique_variable_name() -> str:
"""Get a unique variable name.
@ -78,34 +91,34 @@ def get_unique_variable_name() -> str:
return get_unique_variable_name()
class Var(ABC):
class Var:
"""An abstract var."""
# The name of the var.
name: str
_var_name: str
# The type of the var.
type_: Type
_var_type: Type
# The name of the enclosing state.
state: str = ""
_var_state: str
# Whether this is a local javascript variable.
is_local: bool = False
_var_is_local: bool
# Whether the var is a string literal.
is_string: bool = False
_var_is_string: bool
@classmethod
def create(
cls, value: Any, is_local: bool = True, is_string: bool = False
cls, value: Any, _var_is_local: bool = True, _var_is_string: bool = False
) -> Var | None:
"""Create a var from a value.
Args:
value: The value to create the var from.
is_local: Whether the var is local.
is_string: Whether the var is a string literal.
_var_is_local: Whether the var is local.
_var_is_string: Whether the var is a string literal.
Returns:
The var.
@ -130,23 +143,32 @@ class Var(ABC):
)
name = name if isinstance(name, str) else format.json_dumps(name)
return BaseVar(name=name, type_=type_, is_local=is_local, is_string=is_string)
return BaseVar(
_var_name=name,
_var_type=type_,
_var_is_local=_var_is_local,
_var_is_string=_var_is_string,
)
@classmethod
def create_safe(
cls, value: Any, is_local: bool = True, is_string: bool = False
cls, value: Any, _var_is_local: bool = True, _var_is_string: bool = False
) -> Var:
"""Create a var from a value, guaranteeing that it is not None.
"""Create a var from a value, asserting that it is not None.
Args:
value: The value to create the var from.
is_local: Whether the var is local.
is_string: Whether the var is a string literal.
_var_is_local: Whether the var is local.
_var_is_string: Whether the var is a string literal.
Returns:
The var.
"""
var = cls.create(value, is_local=is_local, is_string=is_string)
var = cls.create(
value,
_var_is_local=_var_is_local,
_var_is_string=_var_is_string,
)
assert var is not None
return var
@ -171,14 +193,14 @@ class Var(ABC):
Returns:
The decoded value or the Var name.
"""
if self.state:
return self.full_name
if self.is_string:
return self.name
if self._var_state:
return self._var_full_name
if self._var_is_string:
return self._var_name
try:
return json.loads(self.name)
return json.loads(self._var_name)
except ValueError:
return self.name
return self._var_name
def equals(self, other: Var) -> bool:
"""Check if two vars are equal.
@ -190,10 +212,10 @@ class Var(ABC):
Whether the vars are equal.
"""
return (
self.name == other.name
and self.type_ == other.type_
and self.state == other.state
and self.is_local == other.is_local
self._var_name == other._var_name
and self._var_type == other._var_type
and self._var_state == other._var_state
and self._var_is_local == other._var_is_local
)
def to_string(self, json: bool = True) -> Var:
@ -214,7 +236,7 @@ class Var(ABC):
Returns:
The hash of the var.
"""
return hash((self.name, str(self.type_)))
return hash((self._var_name, str(self._var_type)))
def __str__(self) -> str:
"""Wrap the var so it can be used in templates.
@ -222,8 +244,12 @@ class Var(ABC):
Returns:
The wrapped var, i.e. {state.var}.
"""
out = self.full_name if self.is_local else format.wrap(self.full_name, "{")
if self.is_string:
out = (
self._var_full_name
if self._var_is_local
else format.wrap(self._var_full_name, "{")
)
if self._var_is_string:
out = format.format_string(out)
return out
@ -234,7 +260,7 @@ class Var(ABC):
TypeError: when attempting to bool-ify the Var.
"""
raise TypeError(
f"Cannot convert Var {self.full_name!r} to bool for use with `if`, `and`, `or`, and `not`. "
f"Cannot convert Var {self._var_full_name!r} to bool for use with `if`, `and`, `or`, and `not`. "
"Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)."
)
@ -245,7 +271,7 @@ class Var(ABC):
TypeError: when attempting to iterate over the Var.
"""
raise TypeError(
f"Cannot iterate over Var {self.full_name!r}. Instead use `rx.foreach`."
f"Cannot iterate over Var {self._var_full_name!r}. Instead use `rx.foreach`."
)
def __format__(self, format_spec: str) -> str:
@ -257,7 +283,7 @@ class Var(ABC):
Returns:
The formatted var.
"""
if self.is_local:
if self._var_is_local:
return str(self)
return f"${str(self)}"
@ -275,16 +301,16 @@ class Var(ABC):
"""
# Indexing is only supported for strings, lists, tuples, dicts, and dataframes.
if not (
types._issubclass(self.type_, Union[List, Dict, Tuple, str])
or types.is_dataframe(self.type_)
types._issubclass(self._var_type, Union[List, Dict, Tuple, str])
or types.is_dataframe(self._var_type)
):
if self.type_ == Any:
if self._var_type == Any:
raise TypeError(
"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."
f"Var {self._var_name} of type {self._var_type} does not support indexing."
)
# The type of the indexed var.
@ -292,15 +318,20 @@ class Var(ABC):
# Convert any vars to local vars.
if isinstance(i, Var):
i = BaseVar(name=i.name, type_=i.type_, state=i.state, is_local=True)
i = BaseVar(
_var_name=i._var_name,
_var_type=i._var_type,
_var_state=i._var_state,
_var_is_local=True,
)
# Handle list/tuple/str indexing.
if types._issubclass(self.type_, Union[List, Tuple, str]):
if types._issubclass(self._var_type, Union[List, Tuple, str]):
# List/Tuple/String indices must be ints, slices, or vars.
if (
not isinstance(i, types.get_args(Union[int, slice, Var]))
or isinstance(i, Var)
and not i.type_ == int
and not i._var_type == int
):
raise TypeError("Index must be an integer or an integer var.")
@ -312,35 +343,40 @@ class Var(ABC):
# Use the slice function.
return BaseVar(
name=f"{self.name}.slice({start}, {stop})",
type_=self.type_,
state=self.state,
is_local=self.is_local,
_var_name=f"{self._var_name}.slice({start}, {stop})",
_var_type=self._var_type,
_var_state=self._var_state,
_var_is_local=self._var_is_local,
)
# Get the type of the indexed var.
type_ = (
types.get_args(self.type_)[0]
if types.is_generic_alias(self.type_)
types.get_args(self._var_type)[0]
if types.is_generic_alias(self._var_type)
else Any
)
# Use `at` to support negative indices.
return BaseVar(
name=f"{self.name}.at({i})",
type_=type_,
state=self.state,
is_local=self.is_local,
_var_name=f"{self._var_name}.at({i})",
_var_type=type_,
_var_state=self._var_state,
_var_is_local=self._var_is_local,
)
# Dictionary / dataframe indexing.
# Tuples are currently not supported as indexes.
if (
(types._issubclass(self.type_, Dict) or types.is_dataframe(self.type_))
(
types._issubclass(self._var_type, Dict)
or types.is_dataframe(self._var_type)
)
and not isinstance(i, types.get_args(Union[int, str, float, Var]))
) or (
isinstance(i, Var)
and not types._issubclass(i.type_, types.get_args(Union[int, str, float]))
and not types._issubclass(
i._var_type, types.get_args(Union[int, str, float])
)
):
raise TypeError(
"Index must be one of the following types: int, str, int or str Var"
@ -349,18 +385,20 @@ class Var(ABC):
if isinstance(i, str):
i = format.wrap(i, '"')
type_ = (
types.get_args(self.type_)[1] if types.is_generic_alias(self.type_) else Any
types.get_args(self._var_type)[1]
if types.is_generic_alias(self._var_type)
else Any
)
# Use normal indexing here.
return BaseVar(
name=f"{self.name}[{i}]",
type_=type_,
state=self.state,
is_local=self.is_local,
_var_name=f"{self._var_name}[{i}]",
_var_type=type_,
_var_state=self._var_state,
_var_is_local=self._var_is_local,
)
def __getattribute__(self, name: str) -> Var:
def __getattr__(self, name: str) -> Var:
"""Get a var attribute.
Args:
@ -373,30 +411,39 @@ class Var(ABC):
AttributeError: If the var is wrongly annotated or can't find attribute.
TypeError: If an annotation to the var isn't provided.
"""
try:
return super().__getattribute__(name)
except Exception as e:
# Check if the attribute is one of the class fields.
if not name.startswith("_"):
if self.type_ == Any:
raise TypeError(
f"You must provide an annotation for the state var `{self.full_name}`. Annotation cannot be `{self.type_}`"
) from None
if hasattr(self.type_, "__fields__") and name in self.type_.__fields__:
type_ = self.type_.__fields__[name].outer_type_
if isinstance(type_, ModelField):
type_ = type_.type_
return BaseVar(
name=f"{self.name}.{name}",
type_=type_,
state=self.state,
is_local=self.is_local,
)
# Check if the attribute is one of the class fields.
if not name.startswith("_"):
if self._var_type == Any:
raise TypeError(
f"You must provide an annotation for the state var `{self._var_full_name}`. Annotation cannot be `{self._var_type}`"
) from None
if (
hasattr(self._var_type, "__fields__")
and name in self._var_type.__fields__
):
type_ = self._var_type.__fields__[name].outer_type_
if isinstance(type_, ModelField):
type_ = type_.type_
return BaseVar(
_var_name=f"{self._var_name}.{name}",
_var_type=type_,
_var_state=self._var_state,
_var_is_local=self._var_is_local,
)
if name in REPLACED_NAMES:
raise AttributeError(
f"Field {name!r} was renamed to {REPLACED_NAMES[name]!r}"
)
raise AttributeError(
f"The State var `{self.full_name}` has no attribute '{name}' or may have been annotated "
f"wrongly.\n"
f"original message: {e.args[0]}"
) from e
f"The State var `{self._var_full_name}` has no attribute '{name}' or may have been annotated "
f"wrongly."
)
raise AttributeError(
f"The State var has no attribute '{name}' or may have been annotated wrongly.",
)
def operation(
self,
@ -429,7 +476,7 @@ class Var(ABC):
else:
other = Var.create(other)
type_ = type_ or self.type_
type_ = type_ or self._var_type
if other is None and flip:
raise ValueError(
@ -441,42 +488,42 @@ class Var(ABC):
if other is not None:
# check if the operation between operands is valid.
if op and not self.is_valid_operation(
types.get_base_class(left_operand.type_), # type: ignore
types.get_base_class(right_operand.type_), # type: ignore
types.get_base_class(left_operand._var_type), # type: ignore
types.get_base_class(right_operand._var_type), # type: ignore
op,
):
raise TypeError(
f"Unsupported Operand type(s) for {op}: `{left_operand.full_name}` of type {left_operand.type_.__name__} and `{right_operand.full_name}` of type {right_operand.type_.__name__}" # type: ignore
f"Unsupported Operand type(s) for {op}: `{left_operand._var_full_name}` of type {left_operand._var_type.__name__} and `{right_operand._var_full_name}` of type {right_operand._var_type.__name__}" # type: ignore
)
# apply function to operands
if fn is not None:
if invoke_fn:
# invoke the function on left operand.
operation_name = f"{left_operand.full_name}.{fn}({right_operand.full_name})" # type: ignore
operation_name = f"{left_operand._var_full_name}.{fn}({right_operand._var_full_name})" # type: ignore
else:
# pass the operands as arguments to the function.
operation_name = f"{left_operand.full_name} {op} {right_operand.full_name}" # type: ignore
operation_name = f"{left_operand._var_full_name} {op} {right_operand._var_full_name}" # type: ignore
operation_name = f"{fn}({operation_name})"
else:
# apply operator to operands (left operand <operator> right_operand)
operation_name = f"{left_operand.full_name} {op} {right_operand.full_name}" # type: ignore
operation_name = f"{left_operand._var_full_name} {op} {right_operand._var_full_name}" # type: ignore
operation_name = format.wrap(operation_name, "(")
else:
# apply operator to left operand (<operator> left_operand)
operation_name = f"{op}{self.full_name}"
operation_name = f"{op}{self._var_full_name}"
# apply function to operands
if fn is not None:
operation_name = (
f"{fn}({operation_name})"
if not invoke_fn
else f"{self.full_name}.{fn}()"
else f"{self._var_full_name}.{fn}()"
)
return BaseVar(
name=operation_name,
type_=type_,
is_local=self.is_local,
_var_name=operation_name,
_var_type=type_,
_var_is_local=self._var_is_local,
)
@staticmethod
@ -554,12 +601,12 @@ class Var(ABC):
Raises:
TypeError: If the var is not a list.
"""
if not types._issubclass(self.type_, List):
if not types._issubclass(self._var_type, List):
raise TypeError(f"Cannot get length of non-list var {self}.")
return BaseVar(
name=f"{self.full_name}.length",
type_=int,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.length",
_var_type=int,
_var_is_local=self._var_is_local,
)
def __eq__(self, other: Var) -> Var:
@ -638,12 +685,12 @@ class Var(ABC):
Returns:
A var representing the sum.
"""
other_type = other.type_ if isinstance(other, Var) else type(other)
other_type = other._var_type if isinstance(other, Var) else type(other)
# For list-list addition, javascript concatenates the content of the lists instead of
# merging the list, and for that reason we use the spread operator available through spreadArraysOrObjects
# utility function
if (
types.get_base_class(self.type_) == list
types.get_base_class(self._var_type) == list
and types.get_base_class(other_type) == list
):
return self.operation(",", other, fn="spreadArraysOrObjects", flip=flip)
@ -692,10 +739,10 @@ class Var(ABC):
Returns:
A var representing the product.
"""
other_type = other.type_ if isinstance(other, Var) else type(other)
other_type = other._var_type if isinstance(other, Var) else type(other)
# For str-int multiplication, we use the repeat function.
# i.e "hello" * 2 is equivalent to "hello".repeat(2) in js.
if (types.get_base_class(self.type_), types.get_base_class(other_type)) in [
if (types.get_base_class(self._var_type), types.get_base_class(other_type)) in [
(int, str),
(str, int),
]:
@ -703,16 +750,16 @@ class Var(ABC):
# For list-int multiplication, we use the Array function.
# i.e ["hello"] * 2 is equivalent to Array(2).fill().map(() => ["hello"]).flat() in js.
if (types.get_base_class(self.type_), types.get_base_class(other_type)) in [
if (types.get_base_class(self._var_type), types.get_base_class(other_type)) in [
(int, list),
(list, int),
]:
other_name = other.full_name if isinstance(other, Var) else other
name = f"Array({other_name}).fill().map(() => {self.full_name}).flat()"
other_name = other._var_full_name if isinstance(other, Var) else other
name = f"Array({other_name}).fill().map(() => {self._var_full_name}).flat()"
return BaseVar(
name=name,
type_=str,
is_local=self.is_local,
_var_name=name,
_var_type=str,
_var_is_local=self._var_is_local,
)
return self.operation("*", other)
@ -830,7 +877,7 @@ class Var(ABC):
>>> var1 = Var.create(True)
>>> var2 = Var.create(False)
>>> js_code = var1 & var2
>>> print(js_code.full_name)
>>> print(js_code._var_full_name)
'(true && false)'
"""
return self.operation("&&", other, type_=bool)
@ -860,7 +907,7 @@ class Var(ABC):
>>> var1 = Var.create(True)
>>> var2 = Var.create(False)
>>> js_code = var1 & var2
>>> print(js_code.full_name)
>>> print(js_code._var_full_name)
'(false && true)'
"""
return self.operation("&&", other, type_=bool, flip=True)
@ -888,7 +935,7 @@ class Var(ABC):
>>> var1 = Var.create(True)
>>> var2 = Var.create(False)
>>> js_code = var1 | var2
>>> print(js_code.full_name)
>>> print(js_code._var_full_name)
'(true || false)'
"""
return self.operation("||", other, type_=bool)
@ -943,35 +990,37 @@ class Var(ABC):
Returns:
A var representing the contain check.
"""
if not (types._issubclass(self.type_, Union[dict, list, tuple, str])):
if not (types._issubclass(self._var_type, Union[dict, list, tuple, str])):
raise TypeError(
f"Var {self.full_name} of type {self.type_} does not support contains check."
f"Var {self._var_full_name} of type {self._var_type} does not support contains check."
)
method = (
"hasOwnProperty" if types.get_base_class(self.type_) == dict else "includes"
"hasOwnProperty"
if types.get_base_class(self._var_type) == dict
else "includes"
)
if isinstance(other, str):
other = Var.create(json.dumps(other), is_string=True)
other = Var.create(json.dumps(other), _var_is_string=True)
elif not isinstance(other, Var):
other = Var.create(other)
if types._issubclass(self.type_, Dict):
if types._issubclass(self._var_type, Dict):
return BaseVar(
name=f"{self.full_name}.{method}({other.full_name})",
type_=bool,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.{method}({other._var_full_name})",
_var_type=bool,
_var_is_local=self._var_is_local,
)
else: # str, list, tuple
# For strings, the left operand must be a string.
if types._issubclass(self.type_, str) and not types._issubclass(
other.type_, str
if types._issubclass(self._var_type, str) and not types._issubclass(
other._var_type, str
):
raise TypeError(
f"'in <string>' requires string as left operand, not {other.type_}"
f"'in <string>' requires string as left operand, not {other._var_type}"
)
return BaseVar(
name=f"{self.full_name}.includes({other.full_name})",
type_=bool,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.includes({other._var_full_name})",
_var_type=bool,
_var_is_local=self._var_is_local,
)
def reverse(self) -> Var:
@ -983,13 +1032,13 @@ class Var(ABC):
Returns:
A var with the reversed list.
"""
if not types._issubclass(self.type_, list):
raise TypeError(f"Cannot reverse non-list var {self.full_name}.")
if not types._issubclass(self._var_type, list):
raise TypeError(f"Cannot reverse non-list var {self._var_full_name}.")
return BaseVar(
name=f"[...{self.full_name}].reverse()",
type_=self.type_,
is_local=self.is_local,
_var_name=f"[...{self._var_full_name}].reverse()",
_var_type=self._var_type,
_var_is_local=self._var_is_local,
)
def lower(self) -> Var:
@ -1001,15 +1050,15 @@ class Var(ABC):
Raises:
TypeError: If the var is not a string.
"""
if not types._issubclass(self.type_, str):
if not types._issubclass(self._var_type, str):
raise TypeError(
f"Cannot convert non-string var {self.full_name} to lowercase."
f"Cannot convert non-string var {self._var_full_name} to lowercase."
)
return BaseVar(
name=f"{self.full_name}.toLowerCase()",
type_=str,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.toLowerCase()",
_var_type=str,
_var_is_local=self._var_is_local,
)
def upper(self) -> Var:
@ -1021,15 +1070,15 @@ class Var(ABC):
Raises:
TypeError: If the var is not a string.
"""
if not types._issubclass(self.type_, str):
if not types._issubclass(self._var_type, str):
raise TypeError(
f"Cannot convert non-string var {self.full_name} to uppercase."
f"Cannot convert non-string var {self._var_full_name} to uppercase."
)
return BaseVar(
name=f"{self.full_name}.toUpperCase()",
type_=str,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.toUpperCase()",
_var_type=str,
_var_is_local=self._var_is_local,
)
def split(self, other: str | Var[str] = " ") -> Var:
@ -1044,15 +1093,15 @@ class Var(ABC):
Raises:
TypeError: If the var is not a string.
"""
if not types._issubclass(self.type_, str):
raise TypeError(f"Cannot split non-string var {self.full_name}.")
if not types._issubclass(self._var_type, str):
raise TypeError(f"Cannot split non-string var {self._var_full_name}.")
other = Var.create_safe(json.dumps(other)) if isinstance(other, str) else other
return BaseVar(
name=f"{self.full_name}.split({other.full_name})",
type_=list[str],
is_local=self.is_local,
_var_name=f"{self._var_full_name}.split({other._var_full_name})",
_var_type=list[str],
_var_is_local=self._var_is_local,
)
def join(self, other: str | Var[str] | None = None) -> Var:
@ -1067,8 +1116,8 @@ class Var(ABC):
Raises:
TypeError: If the var is not a list.
"""
if not types._issubclass(self.type_, list):
raise TypeError(f"Cannot join non-list var {self.full_name}.")
if not types._issubclass(self._var_type, list):
raise TypeError(f"Cannot join non-list var {self._var_full_name}.")
if other is None:
other = Var.create_safe("")
@ -1078,9 +1127,9 @@ class Var(ABC):
other = Var.create_safe(other)
return BaseVar(
name=f"{self.full_name}.join({other.full_name})",
type_=str,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.join({other._var_full_name})",
_var_type=str,
_var_is_local=self._var_is_local,
)
def foreach(self, fn: Callable) -> Var:
@ -1093,13 +1142,13 @@ class Var(ABC):
A var representing foreach operation.
"""
arg = BaseVar(
name=get_unique_variable_name(),
type_=self.type_,
_var_name=get_unique_variable_name(),
_var_type=self._var_type,
)
return BaseVar(
name=f"{self.full_name}.map(({arg.name}, i) => {fn(arg, key='i')})",
type_=self.type_,
is_local=self.is_local,
_var_name=f"{self._var_full_name}.map(({arg._var_name}, i) => {fn(arg, key='i')})",
_var_type=self._var_type,
_var_is_local=self._var_is_local,
)
def to(self, type_: Type) -> Var:
@ -1112,22 +1161,26 @@ class Var(ABC):
The converted var.
"""
return BaseVar(
name=self.name,
type_=type_,
state=self.state,
is_local=self.is_local,
_var_name=self._var_name,
_var_type=type_,
_var_state=self._var_state,
_var_is_local=self._var_is_local,
)
@property
def full_name(self) -> str:
def _var_full_name(self) -> str:
"""Get the full name of the var.
Returns:
The full name of the var.
"""
return self.name if self.state == "" else ".".join([self.state, self.name])
return (
self._var_name
if self._var_state == ""
else ".".join([self._var_state, self._var_name])
)
def set_state(self, state: Type[State]) -> Any:
def _var_set_state(self, state: Type[State]) -> Any:
"""Set the state of the var.
Args:
@ -1136,27 +1189,31 @@ class Var(ABC):
Returns:
The var with the set state.
"""
self.state = state.get_full_name()
self._var_state = state.get_full_name()
return self
class BaseVar(Var, Base):
@dataclasses.dataclass(
eq=False,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class BaseVar(Var):
"""A base (non-computed) var of the app state."""
# The name of the var.
name: str
_var_name: str = dataclasses.field()
# The type of the var.
type_: Any
_var_type: Type = dataclasses.field(default=Any)
# The name of the enclosing state.
state: str = ""
_var_state: str = dataclasses.field(default="")
# Whether this is a local javascript variable.
is_local: bool = False
_var_is_local: bool = dataclasses.field(default=False)
# Whether this var is a raw string.
is_string: bool = False
# Whether the var is a string literal.
_var_is_string: bool = dataclasses.field(default=False)
def __hash__(self) -> int:
"""Define a hash function for a var.
@ -1164,7 +1221,7 @@ class BaseVar(Var, Base):
Returns:
The hash of the var.
"""
return hash((self.name, str(self.type_)))
return hash((self._var_name, str(self._var_type)))
def get_default_value(self) -> Any:
"""Get the default value of the var.
@ -1176,7 +1233,9 @@ class BaseVar(Var, Base):
ImportError: If the var is a dataframe and pandas is not installed.
"""
type_ = (
self.type_.__origin__ if types.is_generic_alias(self.type_) else self.type_
self._var_type.__origin__
if types.is_generic_alias(self._var_type)
else self._var_type
)
if issubclass(type_, str):
return ""
@ -1210,10 +1269,10 @@ class BaseVar(Var, Base):
Returns:
The name of the setter function.
"""
setter = constants.SETTER_PREFIX + self.name
if not include_state or self.state == "":
setter = constants.SETTER_PREFIX + self._var_name
if not include_state or self._var_state == "":
return setter
return ".".join((self.state, setter))
return ".".join((self._var_state, setter))
def get_setter(self) -> Callable[[State, Any], None]:
"""Get the var's setter function.
@ -1229,46 +1288,59 @@ class BaseVar(Var, Base):
state: The state within which we add the setter function.
value: The value to set.
"""
if self.type_ in [int, float]:
if self._var_type in [int, float]:
try:
value = self.type_(value)
setattr(state, self.name, value)
value = self._var_type(value)
setattr(state, self._var_name, value)
except ValueError:
console.warn(
f"{self.name}: Failed conversion of {value} to '{self.type_.__name__}'. Value not set.",
f"{self._var_name}: Failed conversion of {value} to '{self._var_type.__name__}'. Value not set.",
)
else:
setattr(state, self.name, value)
setattr(state, self._var_name, value)
setter.__qualname__ = self.get_setter_name()
return setter
@dataclasses.dataclass(init=False, eq=False)
class ComputedVar(Var, property):
"""A field with computed getters."""
# Whether to track dependencies and cache computed values
cache: bool = False
_cache: bool = dataclasses.field(default=False)
@property
def name(self) -> str:
"""Get the name of the var.
def __init__(
self,
fget: Callable[[State], Any],
fset: Callable[[State, Any], None] | None = None,
fdel: Callable[[State], Any] | None = None,
doc: str | None = None,
**kwargs,
):
"""Initialize a ComputedVar.
Returns:
The name of the var.
Args:
fget: The getter function.
fset: The setter function.
fdel: The deleter function.
doc: The docstring.
**kwargs: additional attributes to set on the instance
"""
assert self.fget is not None, "Var must have a getter."
return self.fget.__name__
property.__init__(self, fget, fset, fdel, doc)
kwargs["_var_name"] = kwargs.pop("_var_name", fget.__name__)
kwargs["_var_type"] = kwargs.pop("_var_type", self._determine_var_type())
BaseVar.__init__(self, **kwargs) # type: ignore
@property
def cache_attr(self) -> str:
def _cache_attr(self) -> str:
"""Get the attribute used to cache the value on the instance.
Returns:
An attribute name.
"""
return f"__cached_{self.name}"
return f"__cached_{self._var_name}"
def __get__(self, instance, owner):
"""Get the ComputedVar value.
@ -1282,15 +1354,15 @@ class ComputedVar(Var, property):
Returns:
The value of the var for the given instance.
"""
if instance is None or not self.cache:
if instance is None or not self._cache:
return super().__get__(instance, owner)
# handle caching
if not hasattr(instance, self.cache_attr):
setattr(instance, self.cache_attr, super().__get__(instance, owner))
return getattr(instance, self.cache_attr)
if not hasattr(instance, self._cache_attr):
setattr(instance, self._cache_attr, super().__get__(instance, owner))
return getattr(instance, self._cache_attr)
def deps(
def _deps(
self,
objclass: Type,
obj: FunctionType | CodeType | None = None,
@ -1348,7 +1420,7 @@ class ComputedVar(Var, property):
elif self_is_top_of_stack and instruction.opname == "LOAD_METHOD":
# method call on self
d.update(
self.deps(
self._deps(
objclass=objclass,
obj=getattr(objclass, instruction.argval),
)
@ -1359,7 +1431,7 @@ class ComputedVar(Var, property):
# recurse into nested functions / comprehensions, which can reference
# instance attributes from the outer scope
d.update(
self.deps(
self._deps(
objclass=objclass,
obj=instruction.argval,
self_name=self_name,
@ -1375,10 +1447,9 @@ class ComputedVar(Var, property):
instance: the state instance that needs to recompute the value.
"""
with contextlib.suppress(AttributeError):
delattr(instance, self.cache_attr)
delattr(instance, self._cache_attr)
@property
def type_(self):
def _determine_var_type(self) -> Type:
"""Get the type of the var.
Returns:
@ -1403,7 +1474,7 @@ def cached_var(fget: Callable[[Any], Any]) -> ComputedVar:
ComputedVar that is recomputed when dependencies change.
"""
cvar = ComputedVar(fget=fget)
cvar.cache = True
cvar._cache = True
return cvar
@ -1468,11 +1539,13 @@ def get_local_storage(key: Var | str | None = None) -> BaseVar:
removal_version="0.3.0",
)
if key is not None:
if not (isinstance(key, Var) and key.type_ == str) and not isinstance(key, str):
type_ = type(key) if not isinstance(key, Var) else key.type_
if not (isinstance(key, Var) and key._var_type == str) and not isinstance(
key, str
):
type_ = type(key) if not isinstance(key, Var) else key._var_type
raise TypeError(
f"Local storage keys can only be of type `str` or `var` of type `str`. Got `{type_}` instead."
)
key = key.full_name if isinstance(key, Var) else format.wrap(key, "'")
return BaseVar(name=f"localStorage.getItem({key})", type_=str)
return BaseVar(name="getAllLocalStorageItems()", type_=Dict)
key = key._var_full_name if isinstance(key, Var) else format.wrap(key, "'")
return BaseVar(_var_name=f"localStorage.getItem({key})", _var_type=str)
return BaseVar(_var_name="getAllLocalStorageItems()", _var_type=Dict)

View File

@ -1,7 +1,7 @@
""" Generated with stubgen from mypy, then manually edited, do not regen."""
from dataclasses import dataclass
from _typeshed import Incomplete
from abc import ABC
from reflex import constants as constants
from reflex.base import Base as Base
from reflex.state import State as State
@ -23,19 +23,19 @@ USED_VARIABLES: Incomplete
def get_unique_variable_name() -> str: ...
class Var(ABC):
name: str
type_: Type
state: str = ""
is_local: bool = False
is_string: bool = False
class Var:
_var_name: str
_var_type: Type
_var_state: str = ""
_var_is_local: bool = False
_var_is_string: bool = False
@classmethod
def create(
cls, value: Any, is_local: bool = False, is_string: bool = False
cls, value: Any, _var_is_local: bool = False, _var_is_string: bool = False
) -> Optional[Var]: ...
@classmethod
def create_safe(
cls, value: Any, is_local: bool = False, is_string: bool = False
cls, value: Any, _var_is_local: bool = False, _var_is_string: bool = False
) -> Var: ...
@classmethod
def __class_getitem__(cls, type_: str) -> _GenericAlias: ...
@ -87,31 +87,31 @@ class Var(ABC):
def foreach(self, fn: Callable) -> Var: ...
def to(self, type_: Type) -> Var: ...
@property
def full_name(self) -> str: ...
def set_state(self, state: Type[State]) -> Any: ...
def _var_full_name(self) -> str: ...
def _var_set_state(self, state: Type[State]) -> Any: ...
class BaseVar(Var, Base):
name: str
type_: Any
state: str = ""
is_local: bool = False
is_string: bool = False
@dataclass(eq=False)
class BaseVar(Var):
_var_name: str
_var_type: Any
_var_state: str = ""
_var_is_local: bool = False
_var_is_string: bool = False
def __hash__(self) -> int: ...
def get_default_value(self) -> Any: ...
def get_setter_name(self, include_state: bool = ...) -> str: ...
def get_setter(self) -> Callable[[State, Any], None]: ...
@dataclass(init=False)
class ComputedVar(Var):
cache: bool
_var_cache: bool
fget: FunctionType
@property
def name(self) -> str: ...
@property
def cache_attr(self) -> str: ...
def _cache_attr(self) -> str: ...
def __get__(self, instance, owner): ...
def deps(self, objclass: Type, obj: Optional[FunctionType] = ...) -> Set[str]: ...
def _deps(self, objclass: Type, obj: Optional[FunctionType] = ...) -> Set[str]: ...
def mark_dirty(self, instance) -> None: ...
@property
def type_(self): ...
def _determine_var_type(self) -> Type: ...
def __init__(self, func) -> None: ...
def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ...

View File

@ -34,7 +34,7 @@ def test_validate_data_table(data_table_state: rx.Var, expected):
expected: expected var name.
"""
if not types.is_dataframe(data_table_state.data.type_):
if not types.is_dataframe(data_table_state.data._var_type):
data_table_component = DataTable.create(
data=data_table_state.data, columns=data_table_state.columns
)

View File

@ -51,7 +51,9 @@ def test_render_child_props():
)._render()
assert tag.props["sx"] == {"foo": "bar", "baz": "quuc"}
assert tag.props["value"].equals(
BaseVar(name="real", type_=str, is_local=True, is_string=False)
BaseVar(
_var_name="real", _var_type=str, _var_is_local=True, _var_is_string=False
)
)
assert len(tag.props["onChange"].events) == 1
assert tag.props["onChange"].events[0].handler == S.on_change
@ -86,11 +88,13 @@ def test_render_child_props_recursive():
)._render()
assert tag.props["sx"] == {"foo": "bar", "baz": "quuc"}
assert tag.props["value"].equals(
BaseVar(name="outer", type_=str, is_local=True, is_string=False)
BaseVar(
_var_name="outer", _var_type=str, _var_is_local=True, _var_is_string=False
)
)
assert tag.props["forceNotifyOnBlur"].name == "false"
assert tag.props["forceNotifyByEnter"].name == "false"
assert tag.props["debounceTimeout"].name == "42"
assert tag.props["forceNotifyOnBlur"]._var_name == "false"
assert tag.props["forceNotifyByEnter"]._var_name == "false"
assert tag.props["debounceTimeout"]._var_name == "42"
assert len(tag.props["onChange"].events) == 1
assert tag.props["onChange"].events[0].handler == S.on_change
assert tag.contents == ""
@ -102,7 +106,7 @@ def test_full_control_implicit_debounce():
value=S.value,
on_change=S.on_change,
)._render()
assert tag.props["debounceTimeout"].name == "50"
assert tag.props["debounceTimeout"]._var_name == "50"
assert len(tag.props["onChange"].events) == 1
assert tag.props["onChange"].events[0].handler == S.on_change
assert tag.contents == ""
@ -114,7 +118,7 @@ def test_full_control_implicit_debounce_text_area():
value=S.value,
on_change=S.on_change,
)._render()
assert tag.props["debounceTimeout"].name == "50"
assert tag.props["debounceTimeout"]._var_name == "50"
assert len(tag.props["onChange"].events) == 1
assert tag.props["onChange"].events[0].handler == S.on_change
assert tag.contents == ""

View File

@ -105,7 +105,7 @@ def test_cond_no_else():
comp = comp.children[0]
assert isinstance(comp, Cond)
assert comp.cond._decode() is True # type: ignore
assert comp.comp1 == Fragment.create(Text.create("hello"))
assert comp.comp1.render() == Fragment.create(Text.create("hello")).render()
assert comp.comp2 == Fragment.create()
# Props do not support the use of cond without else

View File

@ -186,6 +186,6 @@ def test_foreach_render(state_var, render_fn, render_dict):
rend = component.render()
arg_index = rend["arg_index"]
assert rend["iterable_state"] == render_dict["iterable_state"]
assert arg_index.name == render_dict["arg_index"]
assert arg_index.type_ == int
assert arg_index._var_name == render_dict["arg_index"]
assert arg_index._var_type == int
assert rend["iterable_type"] == render_dict["iterable_type"]

View File

@ -43,7 +43,7 @@ try:
pil_image: The image to serialize.
"""
image = Image.create(src=pil_image)
assert str(image.src.name) == serialize_image(pil_image) # type: ignore
assert str(image.src._var_name) == serialize_image(pil_image) # type: ignore
def test_render(pil_image: Img):
"""Test that rendering an image works.
@ -52,7 +52,7 @@ try:
pil_image: The image to serialize.
"""
image = Image.create(src=pil_image)
assert image.src.is_string # type: ignore
assert image.src._var_is_string # type: ignore
except ImportError:

View File

@ -109,7 +109,7 @@ def test_format_cond_tag():
tag = CondTag(
true_value=dict(Tag(name="h1", contents="True content")),
false_value=dict(Tag(name="h2", contents="False content")),
cond=BaseVar(name="logged_in", type_=bool),
cond=BaseVar(_var_name="logged_in", _var_type=bool),
)
tag_dict = dict(tag)
cond, true_value, false_value = (
@ -117,8 +117,8 @@ def test_format_cond_tag():
tag_dict["true_value"],
tag_dict["false_value"],
)
assert cond.name == "logged_in"
assert cond.type_ == bool
assert cond._var_name == "logged_in"
assert cond._var_type == bool
assert true_value["name"] == "h1"
assert true_value["contents"] == "True content"

View File

@ -250,7 +250,7 @@ def test_add_page_set_route_dynamic(app: App, index_page, windows_platform: bool
app.add_page(index_page, route=route)
assert set(app.pages.keys()) == {"test/[dynamic]"}
assert "dynamic" in app.state.computed_vars
assert app.state.computed_vars["dynamic"].deps(objclass=DefaultState) == {
assert app.state.computed_vars["dynamic"]._deps(objclass=DefaultState) == {
constants.ROUTER_DATA
}
assert constants.ROUTER_DATA in app.state().computed_var_dependencies
@ -869,7 +869,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
app.add_page(index_page, route=route, on_load=DynamicState.on_load) # type: ignore
assert arg_name in app.state.vars
assert arg_name in app.state.computed_vars
assert app.state.computed_vars[arg_name].deps(objclass=DynamicState) == {
assert app.state.computed_vars[arg_name]._deps(objclass=DynamicState) == {
constants.ROUTER_DATA
}
assert constants.ROUTER_DATA in app.state().computed_var_dependencies

View File

@ -197,11 +197,11 @@ def test_base_class_vars(test_state):
continue
prop = getattr(cls, field)
assert isinstance(prop, BaseVar)
assert prop.name == field
assert prop._var_name == field
assert cls.num1.type_ == int
assert cls.num2.type_ == float
assert cls.key.type_ == str
assert cls.num1._var_type == int
assert cls.num2._var_type == float
assert cls.key._var_type == str
def test_computed_class_var(test_state):
@ -211,7 +211,7 @@ def test_computed_class_var(test_state):
test_state: A state.
"""
cls = type(test_state)
vars = [(prop.name, prop.type_) for prop in cls.computed_vars.values()]
vars = [(prop._var_name, prop._var_type) for prop in cls.computed_vars.values()]
assert ("sum", float) in vars
assert ("upper", str) in vars
@ -416,11 +416,13 @@ def test_set_class_var():
"""Test setting the var of a class."""
with pytest.raises(AttributeError):
TestState.num3 # type: ignore
TestState._set_var(BaseVar(name="num3", type_=int).set_state(TestState))
TestState._set_var(
BaseVar(_var_name="num3", _var_type=int)._var_set_state(TestState)
)
var = TestState.num3 # type: ignore
assert var.name == "num3"
assert var.type_ == int
assert var.state == TestState.get_full_name()
assert var._var_name == "num3"
assert var._var_type == int
assert var._var_state == TestState.get_full_name()
def test_set_parent_and_substates(test_state, child_state, grandchild_state):
@ -984,7 +986,7 @@ def test_conditional_computed_vars():
assert ms._dirty_computed_vars(from_vars={"flag"}) == {"rendered_var"}
assert ms._dirty_computed_vars(from_vars={"t2"}) == {"rendered_var"}
assert ms._dirty_computed_vars(from_vars={"t1"}) == {"rendered_var"}
assert ms.computed_vars["rendered_var"].deps(objclass=MainState) == {
assert ms.computed_vars["rendered_var"]._deps(objclass=MainState) == {
"flag",
"t1",
"t2",

View File

@ -16,11 +16,11 @@ from reflex.vars import (
)
test_vars = [
BaseVar(name="prop1", type_=int),
BaseVar(name="key", type_=str),
BaseVar(name="value", type_=str, state="state"),
BaseVar(name="local", type_=str, state="state", is_local=True),
BaseVar(name="local2", type_=str, is_local=True),
BaseVar(_var_name="prop1", _var_type=int),
BaseVar(_var_name="key", _var_type=str),
BaseVar(_var_name="value", _var_type=str, _var_state="state"),
BaseVar(_var_name="local", _var_type=str, _var_state="state", _var_is_local=True),
BaseVar(_var_name="local2", _var_type=str, _var_is_local=True),
]
test_import_vars = [ImportVar(tag="DataGrid"), ImportVar(tag="DataGrid", alias="Grid")]
@ -124,7 +124,7 @@ def test_full_name(prop, expected):
prop: The var to test.
expected: The expected full name.
"""
assert prop.full_name == expected
assert prop._var_full_name == expected
@pytest.mark.parametrize(
@ -147,14 +147,14 @@ def test_str(prop, expected):
@pytest.mark.parametrize(
"prop,expected",
[
(BaseVar(name="p", type_=int), 0),
(BaseVar(name="p", type_=float), 0.0),
(BaseVar(name="p", type_=str), ""),
(BaseVar(name="p", type_=bool), False),
(BaseVar(name="p", type_=list), []),
(BaseVar(name="p", type_=dict), {}),
(BaseVar(name="p", type_=tuple), ()),
(BaseVar(name="p", type_=set), set()),
(BaseVar(_var_name="p", _var_type=int), 0),
(BaseVar(_var_name="p", _var_type=float), 0.0),
(BaseVar(_var_name="p", _var_type=str), ""),
(BaseVar(_var_name="p", _var_type=bool), False),
(BaseVar(_var_name="p", _var_type=list), []),
(BaseVar(_var_name="p", _var_type=dict), {}),
(BaseVar(_var_name="p", _var_type=tuple), ()),
(BaseVar(_var_name="p", _var_type=set), set()),
],
)
def test_default_value(prop, expected):
@ -194,13 +194,13 @@ def test_get_setter(prop, expected):
"value,expected",
[
(None, None),
(1, BaseVar(name="1", type_=int, is_local=True)),
("key", BaseVar(name="key", type_=str, is_local=True)),
(3.14, BaseVar(name="3.14", type_=float, is_local=True)),
([1, 2, 3], BaseVar(name="[1, 2, 3]", type_=list, is_local=True)),
(1, BaseVar(_var_name="1", _var_type=int, _var_is_local=True)),
("key", BaseVar(_var_name="key", _var_type=str, _var_is_local=True)),
(3.14, BaseVar(_var_name="3.14", _var_type=float, _var_is_local=True)),
([1, 2, 3], BaseVar(_var_name="[1, 2, 3]", _var_type=list, _var_is_local=True)),
(
{"a": 1, "b": 2},
BaseVar(name='{"a": 1, "b": 2}', type_=dict, is_local=True),
BaseVar(_var_name='{"a": 1, "b": 2}', _var_type=dict, _var_is_local=True),
),
],
)
@ -232,9 +232,9 @@ def test_create_type_error():
def v(value) -> Var:
val = (
Var.create(json.dumps(value), is_string=True, is_local=False)
Var.create(json.dumps(value), _var_is_string=True, _var_is_local=False)
if isinstance(value, str)
else Var.create(value, is_local=False)
else Var.create(value, _var_is_local=False)
)
assert val is not None
return val
@ -264,7 +264,8 @@ def test_basic_operations(TestObj):
assert str(v([1, 2, 3])[v(0)]) == "{[1, 2, 3].at(0)}"
assert str(v({"a": 1, "b": 2})["a"]) == '{{"a": 1, "b": 2}["a"]}'
assert (
str(BaseVar(name="foo", state="state", type_=TestObj).bar) == "{state.foo.bar}"
str(BaseVar(_var_name="foo", _var_state="state", _var_type=TestObj).bar)
== "{state.foo.bar}"
)
assert str(abs(v(1))) == "{Math.abs(1)}"
assert str(v([1, 2, 3]).length()) == "{[1, 2, 3].length}"
@ -274,10 +275,13 @@ def test_basic_operations(TestObj):
assert str(v([1, 2, 3]).reverse()) == "{[...[1, 2, 3]].reverse()}"
assert str(v(["1", "2", "3"]).reverse()) == '{[...["1", "2", "3"]].reverse()}'
assert (
str(BaseVar(name="foo", state="state", type_=list).reverse())
str(BaseVar(_var_name="foo", _var_state="state", _var_type=list).reverse())
== "{[...state.foo].reverse()}"
)
assert str(BaseVar(name="foo", type_=list).reverse()) == "{[...foo].reverse()}"
assert (
str(BaseVar(_var_name="foo", _var_type=list).reverse())
== "{[...foo].reverse()}"
)
@pytest.mark.parametrize(
@ -285,12 +289,12 @@ def test_basic_operations(TestObj):
[
(v([1, 2, 3]), "[1, 2, 3]"),
(v(["1", "2", "3"]), '["1", "2", "3"]'),
(BaseVar(name="foo", state="state", type_=list), "state.foo"),
(BaseVar(name="foo", type_=list), "foo"),
(BaseVar(_var_name="foo", _var_state="state", _var_type=list), "state.foo"),
(BaseVar(_var_name="foo", _var_type=list), "foo"),
(v((1, 2, 3)), "[1, 2, 3]"),
(v(("1", "2", "3")), '["1", "2", "3"]'),
(BaseVar(name="foo", state="state", type_=tuple), "state.foo"),
(BaseVar(name="foo", type_=tuple), "foo"),
(BaseVar(_var_name="foo", _var_state="state", _var_type=tuple), "state.foo"),
(BaseVar(_var_name="foo", _var_type=tuple), "foo"),
],
)
def test_list_tuple_contains(var, expected):
@ -298,8 +302,8 @@ def test_list_tuple_contains(var, expected):
assert str(var.contains("1")) == f'{{{expected}.includes("1")}}'
assert str(var.contains(v(1))) == f"{{{expected}.includes(1)}}"
assert str(var.contains(v("1"))) == f'{{{expected}.includes("1")}}'
other_state_var = BaseVar(name="other", state="state", type_=str)
other_var = BaseVar(name="other", type_=str)
other_state_var = BaseVar(_var_name="other", _var_state="state", _var_type=str)
other_var = BaseVar(_var_name="other", _var_type=str)
assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}"
assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}"
@ -308,15 +312,15 @@ def test_list_tuple_contains(var, expected):
"var, expected",
[
(v("123"), json.dumps("123")),
(BaseVar(name="foo", state="state", type_=str), "state.foo"),
(BaseVar(name="foo", type_=str), "foo"),
(BaseVar(_var_name="foo", _var_state="state", _var_type=str), "state.foo"),
(BaseVar(_var_name="foo", _var_type=str), "foo"),
],
)
def test_str_contains(var, expected):
assert str(var.contains("1")) == f'{{{expected}.includes("1")}}'
assert str(var.contains(v("1"))) == f'{{{expected}.includes("1")}}'
other_state_var = BaseVar(name="other", state="state", type_=str)
other_var = BaseVar(name="other", type_=str)
other_state_var = BaseVar(_var_name="other", _var_state="state", _var_type=str)
other_var = BaseVar(_var_name="other", _var_type=str)
assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}"
assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}"
@ -325,8 +329,8 @@ def test_str_contains(var, expected):
"var, expected",
[
(v({"a": 1, "b": 2}), '{"a": 1, "b": 2}'),
(BaseVar(name="foo", state="state", type_=dict), "state.foo"),
(BaseVar(name="foo", type_=dict), "foo"),
(BaseVar(_var_name="foo", _var_state="state", _var_type=dict), "state.foo"),
(BaseVar(_var_name="foo", _var_type=dict), "foo"),
],
)
def test_dict_contains(var, expected):
@ -334,8 +338,8 @@ def test_dict_contains(var, expected):
assert str(var.contains("1")) == f'{{{expected}.hasOwnProperty("1")}}'
assert str(var.contains(v(1))) == f"{{{expected}.hasOwnProperty(1)}}"
assert str(var.contains(v("1"))) == f'{{{expected}.hasOwnProperty("1")}}'
other_state_var = BaseVar(name="other", state="state", type_=str)
other_var = BaseVar(name="other", type_=str)
other_state_var = BaseVar(_var_name="other", _var_state="state", _var_type=str)
other_var = BaseVar(_var_name="other", _var_type=str)
assert (
str(var.contains(other_state_var))
== f"{{{expected}.hasOwnProperty(state.other)}}"
@ -346,9 +350,9 @@ def test_dict_contains(var, expected):
@pytest.mark.parametrize(
"var",
[
BaseVar(name="list", type_=List[int]),
BaseVar(name="tuple", type_=Tuple[int, int]),
BaseVar(name="str", type_=str),
BaseVar(_var_name="list", _var_type=List[int]),
BaseVar(_var_name="tuple", _var_type=Tuple[int, int]),
BaseVar(_var_name="str", _var_type=str),
],
)
def test_var_indexing_lists(var):
@ -358,49 +362,70 @@ def test_var_indexing_lists(var):
var : The str, list or tuple base var.
"""
# Test basic indexing.
assert str(var[0]) == f"{{{var.name}.at(0)}}"
assert str(var[1]) == f"{{{var.name}.at(1)}}"
assert str(var[0]) == f"{{{var._var_name}.at(0)}}"
assert str(var[1]) == f"{{{var._var_name}.at(1)}}"
# Test negative indexing.
assert str(var[-1]) == f"{{{var.name}.at(-1)}}"
assert str(var[-1]) == f"{{{var._var_name}.at(-1)}}"
@pytest.mark.parametrize(
"var, index",
[
(BaseVar(name="lst", type_=List[int]), [1, 2]),
(BaseVar(name="lst", type_=List[int]), {"name": "dict"}),
(BaseVar(name="lst", type_=List[int]), {"set"}),
(BaseVar(_var_name="lst", _var_type=List[int]), [1, 2]),
(BaseVar(_var_name="lst", _var_type=List[int]), {"name": "dict"}),
(BaseVar(_var_name="lst", _var_type=List[int]), {"set"}),
(
BaseVar(name="lst", type_=List[int]),
BaseVar(_var_name="lst", _var_type=List[int]),
(
1,
2,
),
),
(BaseVar(name="lst", type_=List[int]), 1.5),
(BaseVar(name="lst", type_=List[int]), "str"),
(BaseVar(name="lst", type_=List[int]), BaseVar(name="string_var", type_=str)),
(BaseVar(name="lst", type_=List[int]), BaseVar(name="float_var", type_=float)),
(BaseVar(_var_name="lst", _var_type=List[int]), 1.5),
(BaseVar(_var_name="lst", _var_type=List[int]), "str"),
(
BaseVar(name="lst", type_=List[int]),
BaseVar(name="list_var", type_=List[int]),
BaseVar(_var_name="lst", _var_type=List[int]),
BaseVar(_var_name="string_var", _var_type=str),
),
(BaseVar(name="lst", type_=List[int]), BaseVar(name="set_var", type_=Set[str])),
(
BaseVar(name="lst", type_=List[int]),
BaseVar(name="dict_var", type_=Dict[str, str]),
BaseVar(_var_name="lst", _var_type=List[int]),
BaseVar(_var_name="float_var", _var_type=float),
),
(
BaseVar(_var_name="lst", _var_type=List[int]),
BaseVar(_var_name="list_var", _var_type=List[int]),
),
(
BaseVar(_var_name="lst", _var_type=List[int]),
BaseVar(_var_name="set_var", _var_type=Set[str]),
),
(
BaseVar(_var_name="lst", _var_type=List[int]),
BaseVar(_var_name="dict_var", _var_type=Dict[str, str]),
),
(BaseVar(_var_name="str", _var_type=str), [1, 2]),
(BaseVar(_var_name="lst", _var_type=str), {"name": "dict"}),
(BaseVar(_var_name="lst", _var_type=str), {"set"}),
(
BaseVar(_var_name="lst", _var_type=str),
BaseVar(_var_name="string_var", _var_type=str),
),
(
BaseVar(_var_name="lst", _var_type=str),
BaseVar(_var_name="float_var", _var_type=float),
),
(BaseVar(_var_name="str", _var_type=Tuple[str]), [1, 2]),
(BaseVar(_var_name="lst", _var_type=Tuple[str]), {"name": "dict"}),
(BaseVar(_var_name="lst", _var_type=Tuple[str]), {"set"}),
(
BaseVar(_var_name="lst", _var_type=Tuple[str]),
BaseVar(_var_name="string_var", _var_type=str),
),
(
BaseVar(_var_name="lst", _var_type=Tuple[str]),
BaseVar(_var_name="float_var", _var_type=float),
),
(BaseVar(name="str", type_=str), [1, 2]),
(BaseVar(name="lst", type_=str), {"name": "dict"}),
(BaseVar(name="lst", type_=str), {"set"}),
(BaseVar(name="lst", type_=str), BaseVar(name="string_var", type_=str)),
(BaseVar(name="lst", type_=str), BaseVar(name="float_var", type_=float)),
(BaseVar(name="str", type_=Tuple[str]), [1, 2]),
(BaseVar(name="lst", type_=Tuple[str]), {"name": "dict"}),
(BaseVar(name="lst", type_=Tuple[str]), {"set"}),
(BaseVar(name="lst", type_=Tuple[str]), BaseVar(name="string_var", type_=str)),
(BaseVar(name="lst", type_=Tuple[str]), BaseVar(name="float_var", type_=float)),
],
)
def test_var_unsupported_indexing_lists(var, index):
@ -417,9 +442,9 @@ def test_var_unsupported_indexing_lists(var, index):
@pytest.mark.parametrize(
"var",
[
BaseVar(name="lst", type_=List[int]),
BaseVar(name="tuple", type_=Tuple[int, int]),
BaseVar(name="str", type_=str),
BaseVar(_var_name="lst", _var_type=List[int]),
BaseVar(_var_name="tuple", _var_type=Tuple[int, int]),
BaseVar(_var_name="str", _var_type=str),
],
)
def test_var_list_slicing(var):
@ -428,14 +453,14 @@ def test_var_list_slicing(var):
Args:
var : The str, list or tuple base var.
"""
assert str(var[:1]) == f"{{{var.name}.slice(0, 1)}}"
assert str(var[:1]) == f"{{{var.name}.slice(0, 1)}}"
assert str(var[:]) == f"{{{var.name}.slice(0, undefined)}}"
assert str(var[:1]) == f"{{{var._var_name}.slice(0, 1)}}"
assert str(var[:1]) == f"{{{var._var_name}.slice(0, 1)}}"
assert str(var[:]) == f"{{{var._var_name}.slice(0, undefined)}}"
def test_dict_indexing():
"""Test that we can index into dict vars."""
dct = BaseVar(name="dct", type_=Dict[str, int])
dct = BaseVar(_var_name="dct", _var_type=Dict[str, int])
# Check correct indexing.
assert str(dct["a"]) == '{dct["a"]}'
@ -446,66 +471,66 @@ def test_dict_indexing():
"var, index",
[
(
BaseVar(name="dict", type_=Dict[str, str]),
BaseVar(_var_name="dict", _var_type=Dict[str, str]),
[1, 2],
),
(
BaseVar(name="dict", type_=Dict[str, str]),
BaseVar(_var_name="dict", _var_type=Dict[str, str]),
{"name": "dict"},
),
(
BaseVar(name="dict", type_=Dict[str, str]),
BaseVar(_var_name="dict", _var_type=Dict[str, str]),
{"set"},
),
(
BaseVar(name="dict", type_=Dict[str, str]),
BaseVar(_var_name="dict", _var_type=Dict[str, str]),
(
1,
2,
),
),
(
BaseVar(name="lst", type_=Dict[str, str]),
BaseVar(name="list_var", type_=List[int]),
BaseVar(_var_name="lst", _var_type=Dict[str, str]),
BaseVar(_var_name="list_var", _var_type=List[int]),
),
(
BaseVar(name="lst", type_=Dict[str, str]),
BaseVar(name="set_var", type_=Set[str]),
BaseVar(_var_name="lst", _var_type=Dict[str, str]),
BaseVar(_var_name="set_var", _var_type=Set[str]),
),
(
BaseVar(name="lst", type_=Dict[str, str]),
BaseVar(name="dict_var", type_=Dict[str, str]),
BaseVar(_var_name="lst", _var_type=Dict[str, str]),
BaseVar(_var_name="dict_var", _var_type=Dict[str, str]),
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(_var_name="df", _var_type=DataFrame),
[1, 2],
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(_var_name="df", _var_type=DataFrame),
{"name": "dict"},
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(_var_name="df", _var_type=DataFrame),
{"set"},
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(_var_name="df", _var_type=DataFrame),
(
1,
2,
),
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(name="list_var", type_=List[int]),
BaseVar(_var_name="df", _var_type=DataFrame),
BaseVar(_var_name="list_var", _var_type=List[int]),
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(name="set_var", type_=Set[str]),
BaseVar(_var_name="df", _var_type=DataFrame),
BaseVar(_var_name="set_var", _var_type=Set[str]),
),
(
BaseVar(name="df", type_=DataFrame),
BaseVar(name="dict_var", type_=Dict[str, str]),
BaseVar(_var_name="df", _var_type=DataFrame),
BaseVar(_var_name="dict_var", _var_type=Dict[str, str]),
),
],
)
@ -577,8 +602,7 @@ def test_computed_var_with_annotation_error(request, fixture, full_name):
state.var_with_annotation.foo
assert (
err.value.args[0]
== f"The State var `{full_name}` has no attribute 'foo' or may have been annotated wrongly.\n"
f"original message: 'ComputedVar' object has no attribute 'foo'"
== f"The State var `{full_name}` has no attribute 'foo' or may have been annotated wrongly."
)
@ -605,16 +629,19 @@ def test_import_var(import_var, expected):
@pytest.mark.parametrize(
"key, expected",
[
("test_key", BaseVar(name="localStorage.getItem('test_key')", type_=str)),
(
BaseVar(name="key_var", type_=str),
BaseVar(name="localStorage.getItem(key_var)", type_=str),
"test_key",
BaseVar(_var_name="localStorage.getItem('test_key')", _var_type=str),
),
(
BaseVar(_var_name="key_var", _var_type=str),
BaseVar(_var_name="localStorage.getItem(key_var)", _var_type=str),
),
(
BaseState.val,
BaseVar(name="localStorage.getItem(base_state.val)", type_=str),
BaseVar(_var_name="localStorage.getItem(base_state.val)", _var_type=str),
),
(None, BaseVar(name="getAllLocalStorageItems()", type_=Dict)),
(None, BaseVar(_var_name="getAllLocalStorageItems()", _var_type=Dict)),
],
)
def test_get_local_storage(key, expected):
@ -626,8 +653,8 @@ def test_get_local_storage(key, expected):
"""
local_storage = get_local_storage(key)
assert local_storage.name == expected.name
assert local_storage.type_ == expected.type_
assert local_storage._var_name == expected._var_name
assert local_storage._var_type == expected._var_type
@pytest.mark.parametrize(
@ -636,8 +663,8 @@ def test_get_local_storage(key, expected):
["list", "values"],
{"name": "dict"},
10,
BaseVar(name="key_var", type_=List),
BaseVar(name="key_var", type_=Dict[str, str]),
BaseVar(_var_name="key_var", _var_type=List),
BaseVar(_var_name="key_var", _var_type=Dict[str, str]),
],
)
def test_get_local_storage_raise_error(key):
@ -648,7 +675,7 @@ def test_get_local_storage_raise_error(key):
"""
with pytest.raises(TypeError) as err:
get_local_storage(key)
type_ = type(key) if not isinstance(key, Var) else key.type_
type_ = type(key) if not isinstance(key, Var) else key._var_type
assert (
err.value.args[0]
== f"Local storage keys can only be of type `str` or `var` of type `str`. Got `{type_}` instead."
@ -658,13 +685,13 @@ def test_get_local_storage_raise_error(key):
@pytest.mark.parametrize(
"out, expected",
[
(f"{BaseVar(name='var', type_=str)}", "${var}"),
(f"{BaseVar(_var_name='var', _var_type=str)}", "${var}"),
(
f"testing f-string with {BaseVar(name='myvar', state='state', type_=int)}",
f"testing f-string with {BaseVar(_var_name='myvar', _var_state='state', _var_type=int)}",
"testing f-string with ${state.myvar}",
),
(
f"testing local f-string {BaseVar(name='x', is_local=True, type_=str)}",
f"testing local f-string {BaseVar(_var_name='x', _var_is_local=True, _var_type=str)}",
"testing local f-string x",
),
],
@ -676,14 +703,14 @@ def test_fstrings(out, expected):
@pytest.mark.parametrize(
"var",
[
BaseVar(name="var", type_=int),
BaseVar(name="var", type_=float),
BaseVar(name="var", type_=str),
BaseVar(name="var", type_=bool),
BaseVar(name="var", type_=dict),
BaseVar(name="var", type_=tuple),
BaseVar(name="var", type_=set),
BaseVar(name="var", type_=None),
BaseVar(_var_name="var", _var_type=int),
BaseVar(_var_name="var", _var_type=float),
BaseVar(_var_name="var", _var_type=str),
BaseVar(_var_name="var", _var_type=bool),
BaseVar(_var_name="var", _var_type=dict),
BaseVar(_var_name="var", _var_type=tuple),
BaseVar(_var_name="var", _var_type=set),
BaseVar(_var_name="var", _var_type=None),
],
)
def test_unsupported_types_for_reverse(var):
@ -700,11 +727,11 @@ def test_unsupported_types_for_reverse(var):
@pytest.mark.parametrize(
"var",
[
BaseVar(name="var", type_=int),
BaseVar(name="var", type_=float),
BaseVar(name="var", type_=bool),
BaseVar(name="var", type_=set),
BaseVar(name="var", type_=None),
BaseVar(_var_name="var", _var_type=int),
BaseVar(_var_name="var", _var_type=float),
BaseVar(_var_name="var", _var_type=bool),
BaseVar(_var_name="var", _var_type=set),
BaseVar(_var_name="var", _var_type=None),
],
)
def test_unsupported_types_for_contains(var):
@ -717,34 +744,34 @@ def test_unsupported_types_for_contains(var):
assert var.contains(1)
assert (
err.value.args[0]
== f"Var var of type {var.type_} does not support contains check."
== f"Var var of type {var._var_type} does not support contains check."
)
@pytest.mark.parametrize(
"other",
[
BaseVar(name="other", type_=int),
BaseVar(name="other", type_=float),
BaseVar(name="other", type_=bool),
BaseVar(name="other", type_=list),
BaseVar(name="other", type_=dict),
BaseVar(name="other", type_=tuple),
BaseVar(name="other", type_=set),
BaseVar(_var_name="other", _var_type=int),
BaseVar(_var_name="other", _var_type=float),
BaseVar(_var_name="other", _var_type=bool),
BaseVar(_var_name="other", _var_type=list),
BaseVar(_var_name="other", _var_type=dict),
BaseVar(_var_name="other", _var_type=tuple),
BaseVar(_var_name="other", _var_type=set),
],
)
def test_unsupported_types_for_string_contains(other):
with pytest.raises(TypeError) as err:
assert BaseVar(name="var", type_=str).contains(other)
assert BaseVar(_var_name="var", _var_type=str).contains(other)
assert (
err.value.args[0]
== f"'in <string>' requires string as left operand, not {other.type_}"
== f"'in <string>' requires string as left operand, not {other._var_type}"
)
def test_unsupported_default_contains():
with pytest.raises(TypeError) as err:
assert 1 in BaseVar(name="var", type_=str)
assert 1 in BaseVar(_var_name="var", _var_type=str)
assert (
err.value.args[0]
== "'in' operator not supported for Var types, use Var.contains() instead."

View File

@ -211,11 +211,11 @@ def test_format_string(input: str, output: str):
"input,output",
[
(Var.create(value="test"), "{`test`}"),
(Var.create(value="test", is_local=True), "{`test`}"),
(Var.create(value="test", is_local=False), "{test}"),
(Var.create(value="test", is_string=True), "{`test`}"),
(Var.create(value="test", is_string=False), "{`test`}"),
(Var.create(value="test", is_local=False, is_string=False), "{test}"),
(Var.create(value="test", _var_is_local=True), "{`test`}"),
(Var.create(value="test", _var_is_local=False), "{test}"),
(Var.create(value="test", _var_is_string=True), "{`test`}"),
(Var.create(value="test", _var_is_string=False), "{`test`}"),
(Var.create(value="test", _var_is_local=False, _var_is_string=False), "{test}"),
],
)
def test_format_var(input: Var, output: str):
@ -285,7 +285,7 @@ def test_format_cond(condition: str, true_value: str, false_value: str, expected
(
{
"a": 'foo "{ "bar" }" baz',
"b": BaseVar(name="val", type_="str"),
"b": BaseVar(_var_name="val", _var_type="str"),
},
r'{{"a": "foo \"{ \"bar\" }\" baz", "b": val}}',
),
@ -308,22 +308,25 @@ def test_format_cond(condition: str, true_value: str, false_value: str, expected
'{_e => addEvents([Event("mock_event", {arg:_e.target.value})], _e)}',
),
({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
(BaseVar(name="var", type_="int"), "{var}"),
(BaseVar(_var_name="var", _var_type="int"), "{var}"),
(
BaseVar(
name="_",
type_=Any,
state="",
is_local=True,
is_string=False,
_var_name="_",
_var_type=Any,
_var_state="",
_var_is_local=True,
_var_is_string=False,
),
"{_}",
),
(BaseVar(name='state.colors["a"]', type_="str"), '{state.colors["a"]}'),
({"a": BaseVar(name="val", type_="str")}, '{{"a": val}}'),
({"a": BaseVar(name='"val"', type_="str")}, '{{"a": "val"}}'),
(
{"a": BaseVar(name='state.colors["val"]', type_="str")},
BaseVar(_var_name='state.colors["a"]', _var_type="str"),
'{state.colors["a"]}',
),
({"a": BaseVar(_var_name="val", _var_type="str")}, '{{"a": val}}'),
({"a": BaseVar(_var_name='"val"', _var_type="str")}, '{{"a": "val"}}'),
(
{"a": BaseVar(_var_name='state.colors["val"]', _var_type="str")},
'{{"a": state.colors["val"]}}',
),
# tricky real-world case from markdown component

View File

@ -105,16 +105,16 @@ def test_add_serializer():
([1, 2, 3], "[1, 2, 3]"),
([1, "2", 3.0], '[1, "2", 3.0]'),
(
[1, Var.create_safe("hi"), Var.create_safe("bye", is_local=False)],
[1, Var.create_safe("hi"), Var.create_safe("bye", _var_is_local=False)],
'[1, "hi", bye]',
),
(
(1, Var.create_safe("hi"), Var.create_safe("bye", is_local=False)),
(1, Var.create_safe("hi"), Var.create_safe("bye", _var_is_local=False)),
'[1, "hi", bye]',
),
({1: 2, 3: 4}, '{"1": 2, "3": 4}'),
(
{1: Var.create_safe("hi"), 3: Var.create_safe("bye", is_local=False)},
{1: Var.create_safe("hi"), 3: Var.create_safe("bye", _var_is_local=False)},
'{"1": "hi", "3": bye}',
),
(datetime.datetime(2021, 1, 1, 1, 1, 1, 1), "2021-01-01 01:01:01.000001"),