Files
rio/tests/test_attribute_bindings.py

341 lines
12 KiB
Python

import typing as t
import rio.testing
from rio.observables.observable_property import PendingAttributeBinding
class Parent(rio.Component):
text: str = ""
def build(self) -> rio.Component:
return rio.Text(self.bind().text)
class Grandparent(rio.Component):
text: str = ""
def build(self) -> rio.Component:
return Parent(self.bind().text)
async def test_bindings_arent_created_too_early() -> None:
# There was a time when attribute bindings were created in
# `Component.__init__`, thus skipping any properties that were only assigned
# later.
class IHaveACustomInit(rio.Component):
text: str
def __init__(self, *args, text: str, **kwargs) -> None:
super().__init__(*args, **kwargs)
# `Component.__init__`` has already run, but we haven't assigned
# `self.text` yet. Do it now and assert that it still becomes a
# attribute binding.
self.text = text
def build(self) -> rio.Component:
return rio.Text(self.text)
class Container(rio.Component):
text: str = "hi"
def build(self) -> rio.Component:
return IHaveACustomInit(text=self.bind().text)
async with rio.testing.DummyClient(Container) as test_client:
root_component = test_client.get_component(Container)
child_component = test_client.get_component(IHaveACustomInit)
assert child_component.text == "hi"
root_component.text = "bye"
assert child_component.text == "bye"
async def test_init_receives_attribute_bindings_as_input() -> None:
# For a while we considered initializing attribute bindings before calling a
# component's `__init__` and passing the values of the bindings as arguments
# into `__init__`. But ultimately we decided against it, because some
# components may want to use state properties/bindings in their __init__. So
# make sure the `__init__` actually receives a
# `PleaseTurnThisIntoAStateBinding` object as input.
size_value = None
class Square(rio.Component):
def __init__(self, size: float) -> None:
nonlocal size_value
size_value = size
super().__init__(min_width=size, min_height=size)
def build(self) -> rio.Component:
return rio.Text(
"hi", min_width=self.min_width, min_height=self.min_height
)
class Container(rio.Component):
size: float
def build(self) -> rio.Component:
return Square(self.bind().size)
async with rio.testing.DummyClient(lambda: Container(7)):
assert isinstance(size_value, PendingAttributeBinding)
async def test_binding_assignment_on_child() -> None:
async with rio.testing.DummyClient(Parent) as test_client:
root_component = test_client.get_component(Parent)
text_component = test_client.get_component(rio.Text)
assert not test_client._dirty_components
text_component.text = "Hello"
# Note: The Parent isn't dirty because its `build` never accesses
# `self.text`. It only uses `text` as an attribute binding.
assert test_client._dirty_components == {text_component}
assert text_component.text == "Hello"
assert root_component.text == "Hello"
async def test_binding_assignment_on_parent() -> None:
async with rio.testing.DummyClient(Parent) as test_client:
root_component = test_client.get_component(Parent)
text_component = test_client.get_component(rio.Text)
assert not test_client._dirty_components
root_component.text = "Hello"
# Note: The Parent isn't dirty because its `build` never accesses
# `self.text`. It only uses `text` as an attribute binding.
assert test_client._dirty_components == {text_component}
assert text_component.text == "Hello"
assert root_component.text == "Hello"
async def test_using_binding_and_value() -> None:
# The `Parent` class used in the previous tests only uses `text` as a
# binding, not as a value. This test ensures that the parent is properly
# rebuilt if it depends on the value.
class Hybrid(rio.Component):
text: str = ""
def build(self) -> rio.Component:
return rio.Column(
rio.Text(self.bind().text),
rio.Markdown(self.text),
)
async with rio.testing.DummyClient(Hybrid) as test_client:
root_component = test_client.get_component(Hybrid)
text_component = test_client.get_component(rio.Text)
assert not test_client._dirty_components
text_component.text = "Hello"
# Note: The Markdown component isn't dirty (yet) because its parent
# hasn't been rebuilt yet
assert test_client._dirty_components == {
root_component,
text_component,
}
assert root_component.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_assignment_on_sibling() -> None:
class Root(rio.Component):
text: str = ""
def build(self):
return rio.Column(
rio.Text(self.bind().text),
rio.Text(self.bind().text),
)
async with rio.testing.DummyClient(Root) as test_client:
root_component = test_client.get_component(Root)
text1, text2 = t.cast(
list[rio.Text],
test_client._get_build_output(root_component, rio.Column).children,
)
assert not test_client._dirty_components
text1.text = "Hello"
# Note: The root_component isn't dirty because its `build` never
# accesses `self.text`. It only uses `text` as an attribute binding.
assert test_client._dirty_components == {text1, text2}
assert root_component.text == "Hello"
assert text1.text == "Hello"
assert text2.text == "Hello"
async def test_binding_assignment_on_grandchild() -> None:
async with rio.testing.DummyClient(Grandparent) as test_client:
root_component = test_client.get_component(Grandparent)
parent = t.cast(Parent, test_client._get_build_output(root_component))
text_component: rio.Text = test_client._get_build_output(parent)
assert not test_client._dirty_components
text_component.text = "Hello"
# Note: The Parent and GrandParent aren't dirty because their `build`
# never accesses `self.text`. It only uses `text` as an attribute
# binding.
assert test_client._dirty_components == {text_component}
assert root_component.text == "Hello"
assert parent.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_assignment_on_middle() -> None:
async with rio.testing.DummyClient(Grandparent) as test_client:
root_component = test_client.get_component(Grandparent)
parent: Parent = test_client._get_build_output(root_component)
text_component: rio.Text = test_client._get_build_output(parent)
assert not test_client._dirty_components
parent.text = "Hello"
# Note: The Parent and GrandParent aren't dirty because their `build`
# never accesses `self.text`. It only uses `text` as an attribute
# binding.
assert test_client._dirty_components == {text_component}
assert root_component.text == "Hello"
assert parent.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_assignment_on_child_after_reconciliation() -> None:
async with rio.testing.DummyClient(Parent) as test_client:
root_component = test_client.get_component(Parent)
text_component: rio.Text = test_client._get_build_output(root_component)
assert not test_client._dirty_components
# Rebuild the root component, which reconciles the child
await root_component._force_refresh_()
text_component.text = "Hello"
# Note: The Parent isn't dirty because its `build` never accesses
# `self.text`. It only uses `text` as an attribute binding.
assert test_client._dirty_components == {text_component}
assert root_component.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_assignment_on_parent_after_reconciliation() -> None:
async with rio.testing.DummyClient(Parent) as test_client:
root_component = test_client.get_component(Parent)
text_component: rio.Text = test_client._get_build_output(root_component)
assert not test_client._dirty_components
# Rebuild the root component, which reconciles the child
await root_component._force_refresh_()
root_component.text = "Hello"
# Note: The Parent isn't dirty because its `build` never accesses
# `self.text`. It only uses `text` as an attribute binding.
assert test_client._dirty_components == {text_component}
assert root_component.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_assignment_on_sibling_after_reconciliation() -> None:
class Root(rio.Component):
text: str = ""
def build(self):
return rio.Column(
rio.Text(self.bind().text),
rio.Text(self.bind().text),
)
async with rio.testing.DummyClient(Root) as test_client:
root_component = test_client.get_component(Root)
text1, text2 = test_client._get_build_output(root_component).children
assert not test_client._dirty_components
# Rebuild the root component, which reconciles the children
await root_component._force_refresh_()
text1.text = "Hello"
# Note: The Root isn't dirty because its `build` never accesses
# `self.text`. It only uses `text` as an attribute binding.
assert test_client._dirty_components == {text1, text2}
assert root_component.text == "Hello"
assert text1.text == "Hello"
assert text2.text == "Hello"
async def test_binding_assignment_on_grandchild_after_reconciliation() -> None:
async with rio.testing.DummyClient(Grandparent) as test_client:
root_component = test_client.get_component(Grandparent)
parent: Parent = test_client._get_build_output(root_component)
text_component: rio.Text = test_client._get_build_output(parent)
assert not test_client._dirty_components
# Rebuild the root component, which reconciles the child
await root_component._force_refresh_()
text_component.text = "Hello"
# Note: The Parent and GrandParent aren't dirty because their `build`
# never accesses `self.text`. It only uses `text` as an attribute
# binding.
assert test_client._dirty_components == {text_component}
assert root_component.text == "Hello"
assert parent.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_assignment_on_middle_after_reconciliation() -> None:
async with rio.testing.DummyClient(Grandparent) as test_client:
root_component = test_client.get_component(Grandparent)
parent: Parent = test_client._get_build_output(root_component)
text_component: rio.Text = test_client._get_build_output(parent)
assert not test_client._dirty_components
# Rebuild the root component, which reconciles the child
await root_component._force_refresh_()
parent.text = "Hello"
# Note: The Parent and GrandParent aren't dirty because their `build`
# never accesses `self.text`. It only uses `text` as an attribute
# binding.
assert test_client._dirty_components == {text_component}
assert root_component.text == "Hello"
assert parent.text == "Hello"
assert text_component.text == "Hello"
async def test_binding_to_differently_named_attribute():
class Parent(rio.Component):
foo: str = "" # NOT `text`, which is what TextInput uses
def build(self) -> rio.Component:
return rio.TextInput(text=self.bind().foo)
async with rio.testing.DummyClient(Parent) as test_client:
root_component = test_client.get_component(Parent)
text_input = test_client.get_component(rio.TextInput)
text_input.text = "hi"
assert "foo" in test_client.session._changed_attributes[root_component]