typed mixins and ComponentState (#3196)
* typed mixins * implicit mixin=True kwarg for ComponentState subclasses * fix: always init other subclasses * adjust tests: all mixins support base vars now
This commit is contained in:
parent
87a3ddea7f
commit
d96baac7d9
@ -1,4 +1,5 @@
|
|||||||
"""Test that per-component state scaffold works and operates independently."""
|
"""Test that per-component state scaffold works and operates independently."""
|
||||||
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -45,17 +45,15 @@ def StateInheritance():
|
|||||||
"""Test that state inheritance works as expected."""
|
"""Test that state inheritance works as expected."""
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
class ChildMixin:
|
class ChildMixin(rx.State, mixin=True):
|
||||||
# mixin basevars only work with pydantic/rx.Base models
|
child_mixin: str = "child_mixin"
|
||||||
# child_mixin: str = "child_mixin"
|
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
def computed_child_mixin(self) -> str:
|
def computed_child_mixin(self) -> str:
|
||||||
return "computed_child_mixin"
|
return "computed_child_mixin"
|
||||||
|
|
||||||
class Mixin(ChildMixin):
|
class Mixin(ChildMixin, mixin=True):
|
||||||
# mixin basevars only work with pydantic/rx.Base models
|
mixin: str = "mixin"
|
||||||
# mixin: str = "mixin"
|
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
def computed_mixin(self) -> str:
|
def computed_mixin(self) -> str:
|
||||||
@ -64,7 +62,7 @@ def StateInheritance():
|
|||||||
def on_click_mixin(self):
|
def on_click_mixin(self):
|
||||||
return rx.call_script("alert('clicked')")
|
return rx.call_script("alert('clicked')")
|
||||||
|
|
||||||
class OtherMixin(rx.Base):
|
class OtherMixin(rx.State, mixin=True):
|
||||||
other_mixin: str = "other_mixin"
|
other_mixin: str = "other_mixin"
|
||||||
other_mixin_clicks: int = 0
|
other_mixin_clicks: int = 0
|
||||||
|
|
||||||
@ -78,7 +76,7 @@ def StateInheritance():
|
|||||||
f"{self.__class__.__name__}.clicked.{self.other_mixin_clicks}"
|
f"{self.__class__.__name__}.clicked.{self.other_mixin_clicks}"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Base1(rx.State, Mixin):
|
class Base1(Mixin, rx.State):
|
||||||
_base1: str = "_base1"
|
_base1: str = "_base1"
|
||||||
base1: str = "base1"
|
base1: str = "base1"
|
||||||
|
|
||||||
@ -122,14 +120,15 @@ def StateInheritance():
|
|||||||
|
|
||||||
def index() -> rx.Component:
|
def index() -> rx.Component:
|
||||||
return rx.vstack(
|
return rx.vstack(
|
||||||
rx.chakra.input(
|
rx.input(
|
||||||
id="token", value=Base1.router.session.client_token, is_read_only=True
|
id="token", value=Base1.router.session.client_token, is_read_only=True
|
||||||
),
|
),
|
||||||
# Base 1
|
# Base 1 (Mixin, ChildMixin)
|
||||||
rx.heading(Base1.computed_mixin, id="base1-computed_mixin"),
|
rx.heading(Base1.computed_mixin, id="base1-computed_mixin"),
|
||||||
rx.heading(Base1.computed_basevar, id="base1-computed_basevar"),
|
rx.heading(Base1.computed_basevar, id="base1-computed_basevar"),
|
||||||
rx.heading(Base1.computed_child_mixin, id="base1-child-mixin"),
|
rx.heading(Base1.computed_child_mixin, id="base1-computed-child-mixin"),
|
||||||
rx.heading(Base1.base1, id="base1-base1"),
|
rx.heading(Base1.base1, id="base1-base1"),
|
||||||
|
rx.heading(Base1.child_mixin, id="base1-child-mixin"),
|
||||||
rx.button(
|
rx.button(
|
||||||
"Base1.on_click_mixin",
|
"Base1.on_click_mixin",
|
||||||
on_click=Base1.on_click_mixin, # type: ignore
|
on_click=Base1.on_click_mixin, # type: ignore
|
||||||
@ -138,31 +137,33 @@ def StateInheritance():
|
|||||||
rx.heading(
|
rx.heading(
|
||||||
Base1.computed_backend_vars_base1, id="base1-computed_backend_vars"
|
Base1.computed_backend_vars_base1, id="base1-computed_backend_vars"
|
||||||
),
|
),
|
||||||
# Base 2
|
# Base 2 (no mixins)
|
||||||
rx.heading(Base2.computed_basevar, id="base2-computed_basevar"),
|
rx.heading(Base2.computed_basevar, id="base2-computed_basevar"),
|
||||||
rx.heading(Base2.base2, id="base2-base2"),
|
rx.heading(Base2.base2, id="base2-base2"),
|
||||||
rx.heading(
|
rx.heading(
|
||||||
Base2.computed_backend_vars_base2, id="base2-computed_backend_vars"
|
Base2.computed_backend_vars_base2, id="base2-computed_backend_vars"
|
||||||
),
|
),
|
||||||
# Child 1
|
# Child 1 (Mixin, ChildMixin, OtherMixin)
|
||||||
rx.heading(Child1.computed_basevar, id="child1-computed_basevar"),
|
rx.heading(Child1.computed_basevar, id="child1-computed_basevar"),
|
||||||
rx.heading(Child1.computed_mixin, id="child1-computed_mixin"),
|
rx.heading(Child1.computed_mixin, id="child1-computed_mixin"),
|
||||||
rx.heading(Child1.computed_other_mixin, id="child1-other-mixin"),
|
rx.heading(Child1.computed_other_mixin, id="child1-other-mixin"),
|
||||||
rx.heading(Child1.computed_child_mixin, id="child1-child-mixin"),
|
rx.heading(Child1.computed_child_mixin, id="child1-computed-child-mixin"),
|
||||||
rx.heading(Child1.base1, id="child1-base1"),
|
rx.heading(Child1.base1, id="child1-base1"),
|
||||||
rx.heading(Child1.other_mixin, id="child1-other_mixin"),
|
rx.heading(Child1.other_mixin, id="child1-other_mixin"),
|
||||||
|
rx.heading(Child1.child_mixin, id="child1-child-mixin"),
|
||||||
rx.button(
|
rx.button(
|
||||||
"Child1.on_click_other_mixin",
|
"Child1.on_click_other_mixin",
|
||||||
on_click=Child1.on_click_other_mixin, # type: ignore
|
on_click=Child1.on_click_other_mixin, # type: ignore
|
||||||
id="child1-other-mixin-btn",
|
id="child1-other-mixin-btn",
|
||||||
),
|
),
|
||||||
# Child 2
|
# Child 2 (Mixin, ChildMixin, OtherMixin)
|
||||||
rx.heading(Child2.computed_basevar, id="child2-computed_basevar"),
|
rx.heading(Child2.computed_basevar, id="child2-computed_basevar"),
|
||||||
rx.heading(Child2.computed_mixin, id="child2-computed_mixin"),
|
rx.heading(Child2.computed_mixin, id="child2-computed_mixin"),
|
||||||
rx.heading(Child2.computed_other_mixin, id="child2-other-mixin"),
|
rx.heading(Child2.computed_other_mixin, id="child2-other-mixin"),
|
||||||
rx.heading(Child2.computed_child_mixin, id="child2-child-mixin"),
|
rx.heading(Child2.computed_child_mixin, id="child2-computed-child-mixin"),
|
||||||
rx.heading(Child2.base2, id="child2-base2"),
|
rx.heading(Child2.base2, id="child2-base2"),
|
||||||
rx.heading(Child2.other_mixin, id="child2-other_mixin"),
|
rx.heading(Child2.other_mixin, id="child2-other_mixin"),
|
||||||
|
rx.heading(Child2.child_mixin, id="child2-child-mixin"),
|
||||||
rx.button(
|
rx.button(
|
||||||
"Child2.on_click_mixin",
|
"Child2.on_click_mixin",
|
||||||
on_click=Child2.on_click_mixin, # type: ignore
|
on_click=Child2.on_click_mixin, # type: ignore
|
||||||
@ -173,15 +174,16 @@ def StateInheritance():
|
|||||||
on_click=Child2.on_click_other_mixin, # type: ignore
|
on_click=Child2.on_click_other_mixin, # type: ignore
|
||||||
id="child2-other-mixin-btn",
|
id="child2-other-mixin-btn",
|
||||||
),
|
),
|
||||||
# Child 3
|
# Child 3 (Mixin, ChildMixin, OtherMixin)
|
||||||
rx.heading(Child3.computed_basevar, id="child3-computed_basevar"),
|
rx.heading(Child3.computed_basevar, id="child3-computed_basevar"),
|
||||||
rx.heading(Child3.computed_mixin, id="child3-computed_mixin"),
|
rx.heading(Child3.computed_mixin, id="child3-computed_mixin"),
|
||||||
rx.heading(Child3.computed_other_mixin, id="child3-other-mixin"),
|
rx.heading(Child3.computed_other_mixin, id="child3-other-mixin"),
|
||||||
rx.heading(Child3.computed_childvar, id="child3-computed_childvar"),
|
rx.heading(Child3.computed_childvar, id="child3-computed_childvar"),
|
||||||
rx.heading(Child3.computed_child_mixin, id="child3-child-mixin"),
|
rx.heading(Child3.computed_child_mixin, id="child3-computed-child-mixin"),
|
||||||
rx.heading(Child3.child3, id="child3-child3"),
|
rx.heading(Child3.child3, id="child3-child3"),
|
||||||
rx.heading(Child3.base2, id="child3-base2"),
|
rx.heading(Child3.base2, id="child3-base2"),
|
||||||
rx.heading(Child3.other_mixin, id="child3-other_mixin"),
|
rx.heading(Child3.other_mixin, id="child3-other_mixin"),
|
||||||
|
rx.heading(Child3.child_mixin, id="child3-child-mixin"),
|
||||||
rx.button(
|
rx.button(
|
||||||
"Child3.on_click_mixin",
|
"Child3.on_click_mixin",
|
||||||
on_click=Child3.on_click_mixin, # type: ignore
|
on_click=Child3.on_click_mixin, # type: ignore
|
||||||
@ -282,7 +284,9 @@ def test_state_inheritance(
|
|||||||
base1_computed_basevar = driver.find_element(By.ID, "base1-computed_basevar")
|
base1_computed_basevar = driver.find_element(By.ID, "base1-computed_basevar")
|
||||||
assert base1_computed_basevar.text == "computed_basevar1"
|
assert base1_computed_basevar.text == "computed_basevar1"
|
||||||
|
|
||||||
base1_computed_child_mixin = driver.find_element(By.ID, "base1-child-mixin")
|
base1_computed_child_mixin = driver.find_element(
|
||||||
|
By.ID, "base1-computed-child-mixin"
|
||||||
|
)
|
||||||
assert base1_computed_child_mixin.text == "computed_child_mixin"
|
assert base1_computed_child_mixin.text == "computed_child_mixin"
|
||||||
|
|
||||||
base1_base1 = driver.find_element(By.ID, "base1-base1")
|
base1_base1 = driver.find_element(By.ID, "base1-base1")
|
||||||
@ -293,6 +297,9 @@ def test_state_inheritance(
|
|||||||
)
|
)
|
||||||
assert base1_computed_backend_vars.text == "_base1"
|
assert base1_computed_backend_vars.text == "_base1"
|
||||||
|
|
||||||
|
base1_child_mixin = driver.find_element(By.ID, "base1-child-mixin")
|
||||||
|
assert base1_child_mixin.text == "child_mixin"
|
||||||
|
|
||||||
# Base 2
|
# Base 2
|
||||||
base2_computed_basevar = driver.find_element(By.ID, "base2-computed_basevar")
|
base2_computed_basevar = driver.find_element(By.ID, "base2-computed_basevar")
|
||||||
assert base2_computed_basevar.text == "computed_basevar2"
|
assert base2_computed_basevar.text == "computed_basevar2"
|
||||||
@ -315,7 +322,9 @@ def test_state_inheritance(
|
|||||||
child1_computed_other_mixin = driver.find_element(By.ID, "child1-other-mixin")
|
child1_computed_other_mixin = driver.find_element(By.ID, "child1-other-mixin")
|
||||||
assert child1_computed_other_mixin.text == "other_mixin"
|
assert child1_computed_other_mixin.text == "other_mixin"
|
||||||
|
|
||||||
child1_computed_child_mixin = driver.find_element(By.ID, "child1-child-mixin")
|
child1_computed_child_mixin = driver.find_element(
|
||||||
|
By.ID, "child1-computed-child-mixin"
|
||||||
|
)
|
||||||
assert child1_computed_child_mixin.text == "computed_child_mixin"
|
assert child1_computed_child_mixin.text == "computed_child_mixin"
|
||||||
|
|
||||||
child1_base1 = driver.find_element(By.ID, "child1-base1")
|
child1_base1 = driver.find_element(By.ID, "child1-base1")
|
||||||
@ -324,6 +333,9 @@ def test_state_inheritance(
|
|||||||
child1_other_mixin = driver.find_element(By.ID, "child1-other_mixin")
|
child1_other_mixin = driver.find_element(By.ID, "child1-other_mixin")
|
||||||
assert child1_other_mixin.text == "other_mixin"
|
assert child1_other_mixin.text == "other_mixin"
|
||||||
|
|
||||||
|
child1_child_mixin = driver.find_element(By.ID, "child1-child-mixin")
|
||||||
|
assert child1_child_mixin.text == "child_mixin"
|
||||||
|
|
||||||
# Child 2
|
# Child 2
|
||||||
child2_computed_basevar = driver.find_element(By.ID, "child2-computed_basevar")
|
child2_computed_basevar = driver.find_element(By.ID, "child2-computed_basevar")
|
||||||
assert child2_computed_basevar.text == "computed_basevar2"
|
assert child2_computed_basevar.text == "computed_basevar2"
|
||||||
@ -334,7 +346,9 @@ def test_state_inheritance(
|
|||||||
child2_computed_other_mixin = driver.find_element(By.ID, "child2-other-mixin")
|
child2_computed_other_mixin = driver.find_element(By.ID, "child2-other-mixin")
|
||||||
assert child2_computed_other_mixin.text == "other_mixin"
|
assert child2_computed_other_mixin.text == "other_mixin"
|
||||||
|
|
||||||
child2_computed_child_mixin = driver.find_element(By.ID, "child2-child-mixin")
|
child2_computed_child_mixin = driver.find_element(
|
||||||
|
By.ID, "child2-computed-child-mixin"
|
||||||
|
)
|
||||||
assert child2_computed_child_mixin.text == "computed_child_mixin"
|
assert child2_computed_child_mixin.text == "computed_child_mixin"
|
||||||
|
|
||||||
child2_base2 = driver.find_element(By.ID, "child2-base2")
|
child2_base2 = driver.find_element(By.ID, "child2-base2")
|
||||||
@ -343,6 +357,9 @@ def test_state_inheritance(
|
|||||||
child2_other_mixin = driver.find_element(By.ID, "child2-other_mixin")
|
child2_other_mixin = driver.find_element(By.ID, "child2-other_mixin")
|
||||||
assert child2_other_mixin.text == "other_mixin"
|
assert child2_other_mixin.text == "other_mixin"
|
||||||
|
|
||||||
|
child2_child_mixin = driver.find_element(By.ID, "child2-child-mixin")
|
||||||
|
assert child2_child_mixin.text == "child_mixin"
|
||||||
|
|
||||||
# Child 3
|
# Child 3
|
||||||
child3_computed_basevar = driver.find_element(By.ID, "child3-computed_basevar")
|
child3_computed_basevar = driver.find_element(By.ID, "child3-computed_basevar")
|
||||||
assert child3_computed_basevar.text == "computed_basevar2"
|
assert child3_computed_basevar.text == "computed_basevar2"
|
||||||
@ -356,7 +373,9 @@ def test_state_inheritance(
|
|||||||
child3_computed_childvar = driver.find_element(By.ID, "child3-computed_childvar")
|
child3_computed_childvar = driver.find_element(By.ID, "child3-computed_childvar")
|
||||||
assert child3_computed_childvar.text == "computed_childvar"
|
assert child3_computed_childvar.text == "computed_childvar"
|
||||||
|
|
||||||
child3_computed_child_mixin = driver.find_element(By.ID, "child3-child-mixin")
|
child3_computed_child_mixin = driver.find_element(
|
||||||
|
By.ID, "child3-computed-child-mixin"
|
||||||
|
)
|
||||||
assert child3_computed_child_mixin.text == "computed_child_mixin"
|
assert child3_computed_child_mixin.text == "computed_child_mixin"
|
||||||
|
|
||||||
child3_child3 = driver.find_element(By.ID, "child3-child3")
|
child3_child3 = driver.find_element(By.ID, "child3-child3")
|
||||||
@ -368,6 +387,9 @@ def test_state_inheritance(
|
|||||||
child3_other_mixin = driver.find_element(By.ID, "child3-other_mixin")
|
child3_other_mixin = driver.find_element(By.ID, "child3-other_mixin")
|
||||||
assert child3_other_mixin.text == "other_mixin"
|
assert child3_other_mixin.text == "other_mixin"
|
||||||
|
|
||||||
|
child3_child_mixin = driver.find_element(By.ID, "child3-child-mixin")
|
||||||
|
assert child3_child_mixin.text == "child_mixin"
|
||||||
|
|
||||||
child3_computed_backend_vars = driver.find_element(
|
child3_computed_backend_vars = driver.find_element(
|
||||||
By.ID, "child3-computed_backend_vars"
|
By.ID, "child3-computed_backend_vars"
|
||||||
)
|
)
|
||||||
|
@ -359,6 +359,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
# Whether the state has ever been touched since instantiation.
|
# Whether the state has ever been touched since instantiation.
|
||||||
_was_touched: bool = False
|
_was_touched: bool = False
|
||||||
|
|
||||||
|
# Whether this state class is a mixin and should not be instantiated.
|
||||||
|
_mixin: ClassVar[bool] = False
|
||||||
|
|
||||||
# A special event handler for setting base vars.
|
# A special event handler for setting base vars.
|
||||||
setvar: ClassVar[EventHandler]
|
setvar: ClassVar[EventHandler]
|
||||||
|
|
||||||
@ -428,17 +431,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
v
|
v
|
||||||
for mixin in cls.__mro__
|
for mixin in cls._mixins() + [cls]
|
||||||
if mixin is cls or not issubclass(mixin, (BaseState, ABC))
|
|
||||||
for v in mixin.__dict__.values()
|
for v in mixin.__dict__.values()
|
||||||
if isinstance(v, ComputedVar)
|
if isinstance(v, ComputedVar)
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, mixin: bool = False, **kwargs):
|
||||||
"""Do some magic for the subclass initialization.
|
"""Do some magic for the subclass initialization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
mixin: Whether the subclass is a mixin and should not be initialized.
|
||||||
**kwargs: The kwargs to pass to the pydantic init_subclass method.
|
**kwargs: The kwargs to pass to the pydantic init_subclass method.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@ -447,6 +450,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
from reflex.utils.exceptions import StateValueError
|
from reflex.utils.exceptions import StateValueError
|
||||||
|
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
|
cls._mixin = mixin
|
||||||
|
if mixin:
|
||||||
|
return
|
||||||
|
|
||||||
# Event handlers should not shadow builtin state methods.
|
# Event handlers should not shadow builtin state methods.
|
||||||
cls._check_overridden_methods()
|
cls._check_overridden_methods()
|
||||||
# Computed vars should not shadow builtin state props.
|
# Computed vars should not shadow builtin state props.
|
||||||
@ -618,8 +626,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
return [
|
return [
|
||||||
mixin
|
mixin
|
||||||
for mixin in cls.__mro__
|
for mixin in cls.__mro__
|
||||||
if not issubclass(mixin, (BaseState, ABC))
|
if (
|
||||||
and mixin not in [pydantic.BaseModel, Base]
|
mixin not in [pydantic.BaseModel, Base, cls]
|
||||||
|
and issubclass(mixin, BaseState)
|
||||||
|
and mixin._mixin is True
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -742,7 +753,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
parent_states = [
|
parent_states = [
|
||||||
base
|
base
|
||||||
for base in cls.__bases__
|
for base in cls.__bases__
|
||||||
if types._issubclass(base, BaseState) and base is not BaseState
|
if issubclass(base, BaseState) and base is not BaseState and not base._mixin
|
||||||
]
|
]
|
||||||
assert len(parent_states) < 2, "Only one parent state is allowed."
|
assert len(parent_states) < 2, "Only one parent state is allowed."
|
||||||
return parent_states[0] if len(parent_states) == 1 else None # type: ignore
|
return parent_states[0] if len(parent_states) == 1 else None # type: ignore
|
||||||
@ -1833,7 +1844,7 @@ class OnLoadInternalState(State):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ComponentState(Base):
|
class ComponentState(State, mixin=True):
|
||||||
"""Base class to allow for the creation of a state instance per component.
|
"""Base class to allow for the creation of a state instance per component.
|
||||||
|
|
||||||
This allows for the bundling of UI and state logic into a single class,
|
This allows for the bundling of UI and state logic into a single class,
|
||||||
@ -1875,6 +1886,18 @@ class ComponentState(Base):
|
|||||||
# The number of components created from this class.
|
# The number of components created from this class.
|
||||||
_per_component_state_instance_count: ClassVar[int] = 0
|
_per_component_state_instance_count: ClassVar[int] = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(cls, mixin: bool = False, **kwargs):
|
||||||
|
"""Overwrite mixin default to True.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mixin: Whether the subclass is a mixin and should not be initialized.
|
||||||
|
**kwargs: The kwargs to pass to the pydantic init_subclass method.
|
||||||
|
"""
|
||||||
|
if ComponentState in cls.__bases__:
|
||||||
|
mixin = True
|
||||||
|
super().__init_subclass__(mixin=mixin, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_component(cls, *children, **props) -> "Component":
|
def get_component(cls, *children, **props) -> "Component":
|
||||||
"""Get the component instance.
|
"""Get the component instance.
|
||||||
|
Loading…
Reference in New Issue
Block a user