mirror of
https://github.com/rio-labs/rio.git
synced 2025-12-16 18:25:45 -06:00
improve docs and add tests
This commit is contained in:
@@ -870,6 +870,8 @@ export class MediaPlayerComponent extends KeyboardFocusableComponent<MediaPlayer
|
||||
// Explicitly unload the video, just in case someone is still holding a
|
||||
// reference to this component or element
|
||||
this.mediaPlayer.pause();
|
||||
// "" isn't a valid URL, so disable error reporting
|
||||
this.state.reportError = false;
|
||||
this.mediaPlayer.src = "";
|
||||
this.mediaPlayer.load();
|
||||
}
|
||||
|
||||
@@ -26,9 +26,13 @@ class Image(FundamentalComponent):
|
||||
adapts to any space allocated by its parent component.
|
||||
|
||||
Note that unlike most components in Rio, the `Image` component does not have
|
||||
a `natural` size, since images can be easily be scaled to fit any space.
|
||||
Because of this, `Image` defaults to a width and height of 2. This avoids
|
||||
invisible images when you forget to set the size.
|
||||
a "natural" size, since images can be easily be scaled to fit any space.
|
||||
|
||||
A common mistake when displaying images is forgetting to give them a minimum
|
||||
size, which can cause the image to be invisible. To avoid this, `Image`
|
||||
defaults to a `min_width` and `min_height` of 2. It also displays a loading
|
||||
animation while the image is loading, so users can always tell that there's
|
||||
an image there, even if they can't see the actual image yet.
|
||||
|
||||
The actual picture content can be scaled to fit the assigned shape in one of
|
||||
three ways:
|
||||
|
||||
@@ -66,8 +66,6 @@ class TreeView(Component):
|
||||
children=[rio.SimpleTreeItem("Child", key="child")],
|
||||
),
|
||||
selection_mode="multiple",
|
||||
selected_items=["root"],
|
||||
key="tree1",
|
||||
)
|
||||
```
|
||||
|
||||
@@ -97,7 +95,6 @@ class TreeView(Component):
|
||||
*root_items,
|
||||
selection_mode="multiple",
|
||||
on_selection_change=self.on_selection_change,
|
||||
key="dynamic_tree",
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ def get_all_documented_objects() -> dict[
|
||||
all_docs = get_rio_module_docs().iter_children(
|
||||
include_self=True, recursive=True
|
||||
)
|
||||
all_docs = sorted(all_docs, key=lambda docs: docs.name)
|
||||
return {
|
||||
docs.object: docs
|
||||
for docs in all_docs
|
||||
|
||||
@@ -10,7 +10,6 @@ import types
|
||||
import typing as t
|
||||
import weakref
|
||||
|
||||
import imy.docstrings
|
||||
import introspection
|
||||
import typing_extensions as te
|
||||
|
||||
@@ -324,9 +323,75 @@ class RioDataclassMeta(abc.ABCMeta):
|
||||
cls._observable_properties_[field_name] = prop
|
||||
|
||||
|
||||
@imy.docstrings.mark_as_private
|
||||
class Dataclass(metaclass=RioDataclassMeta):
|
||||
"""
|
||||
Dataclasses that automatically rebuild components when they change.
|
||||
|
||||
Inheriting from this class makes the child class a dataclass. Components
|
||||
that access the attributes of a `Dataclass` instance are automatically
|
||||
rebuilt when that attribute changes.
|
||||
|
||||
This is the same mechanism that powers Rio components, which means there are
|
||||
some differences to regular dataclasses:
|
||||
|
||||
- Mutable default values are allowed. (The constructor makes a deepcopy of
|
||||
the default value.)
|
||||
- No `__eq__` method is generated.
|
||||
- All `__post_init__`s are called automatically, parent first and then
|
||||
child. `InitVar`s are not supported.
|
||||
|
||||
## Examples
|
||||
|
||||
In this example you can see that Rio automatically rebuilds the
|
||||
`EmployeePromoter` component when the employee's rank changes. Without
|
||||
`rio.Dataclass`, you would have to use `self.force_refresh()` to rebuild the
|
||||
component.
|
||||
|
||||
```python
|
||||
class Employee(rio.Dataclass):
|
||||
name: str
|
||||
rank: int = 1
|
||||
|
||||
class EmployeePromoter(rio.Component):
|
||||
employee: Employee
|
||||
|
||||
def _promote(self):
|
||||
self.employee.rank += 1
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
return rio.Column(
|
||||
rio.Text(self.employee.name),
|
||||
rio.Text(f"Rank: {self.employee.rank}"),
|
||||
rio.Button("Promote", on_click=self._promote),
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
# There isn't really a good type annotation for this... `te.Self` is the
|
||||
# closest thing
|
||||
def bind(self) -> te.Self:
|
||||
"""
|
||||
Create an attribute binding between this dataclass instance and a Rio
|
||||
component.
|
||||
|
||||
Attribute bindings allow components to assign values to dataclass
|
||||
instances. Example:
|
||||
|
||||
```python
|
||||
class Person(rio.Dataclass):
|
||||
name: str
|
||||
|
||||
class NameChanger(rio.Component):
|
||||
person: Person
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
return rio.TextInput(
|
||||
# Thanks to the attribute binding, typing in the TextInput
|
||||
# will also update the person's name
|
||||
self.person.bind().name
|
||||
)
|
||||
```
|
||||
|
||||
For more details, see [Attribute Bindings](https://rio.dev/docs/howto/howto-get-value-from-child-component).
|
||||
"""
|
||||
return AttributeBindingMaker(self) # type: ignore
|
||||
|
||||
@@ -1486,11 +1486,6 @@ window.location.href = {json.dumps(str(active_page_url))};
|
||||
This function can only be used in "app" mode. (i.e. not in the browser)
|
||||
|
||||
|
||||
## Parameters
|
||||
|
||||
`multiple`: Whether the user should pick a single folder, or multiple.
|
||||
|
||||
|
||||
## Raises
|
||||
|
||||
`NoFolderSelectedError`: If the user did not select a folder.
|
||||
|
||||
@@ -121,6 +121,10 @@ def i_know_what_im_doing(thing: t.Callable):
|
||||
## Parameters
|
||||
|
||||
`thing`: The function or class to suppress.
|
||||
|
||||
## Metadata
|
||||
|
||||
`decorator`: True
|
||||
"""
|
||||
I_KNOW_WHAT_IM_DOING.add(thing)
|
||||
return thing
|
||||
|
||||
@@ -16,13 +16,13 @@ async def test_change_selection_mode() -> None:
|
||||
selection_mode="single",
|
||||
)
|
||||
) as test_client:
|
||||
await asyncio.sleep(0.5)
|
||||
list_view = test_client.get_component(rio.ListView)
|
||||
item = test_client.get_component(rio.SimpleListItem)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
await test_client.click(10, 1)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
list_view = test_client.get_component(rio.ListView)
|
||||
item = test_client.get_component(rio.SimpleListItem)
|
||||
assert num_presses == 1
|
||||
assert list_view.selected_items == [item.key]
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@ async def test_session_property_change(attr_name: str, new_value: object):
|
||||
value = getattr(self.session, attr_name)
|
||||
return rio.Text(str(value))
|
||||
|
||||
async with rio.testing.DummyClient(TestComponent) as test_client:
|
||||
test_component = test_client.get_component(TestComponent)
|
||||
async with rio.testing.DummyClient(TestComponent) as client:
|
||||
test_component = client.get_component(TestComponent)
|
||||
|
||||
test_client._received_messages.clear()
|
||||
setattr(test_client.session, attr_name, new_value)
|
||||
await test_client.wait_for_refresh()
|
||||
client._received_messages.clear()
|
||||
setattr(client.session, attr_name, new_value)
|
||||
await client.wait_for_refresh()
|
||||
|
||||
# Note: The `Text` component isn't necessarily updated, because the
|
||||
# value we assigned might be the same as before, so the reconciler
|
||||
# doesn't consider it dirty
|
||||
assert test_component in test_client._last_updated_components
|
||||
assert test_component in client._last_updated_components
|
||||
|
||||
|
||||
async def test_session_attachment_change():
|
||||
@@ -39,15 +39,106 @@ async def test_session_attachment_change():
|
||||
|
||||
async with rio.testing.DummyClient(
|
||||
TestComponent, default_attachments=["foo"]
|
||||
) as test_client:
|
||||
test_component = test_client.get_component(TestComponent)
|
||||
text_component = test_client.get_component(rio.Text)
|
||||
) as client:
|
||||
test_component = client.get_component(TestComponent)
|
||||
text_component = client.get_component(rio.Text)
|
||||
|
||||
test_client._received_messages.clear()
|
||||
test_client.session.attach("bar")
|
||||
await test_client.wait_for_refresh()
|
||||
client._received_messages.clear()
|
||||
client.session.attach("bar")
|
||||
await client.wait_for_refresh()
|
||||
|
||||
assert test_client._last_updated_components == {
|
||||
assert client._last_updated_components == {
|
||||
test_component,
|
||||
text_component,
|
||||
}
|
||||
|
||||
|
||||
async def test_list():
|
||||
class ElementAdder(rio.Component):
|
||||
list: rio.List[str]
|
||||
|
||||
def build(self):
|
||||
return rio.Button(
|
||||
"add an element",
|
||||
on_press=lambda: self.list.append("foo"),
|
||||
)
|
||||
|
||||
class Display(rio.Component):
|
||||
list: rio.List[str]
|
||||
|
||||
def build(self):
|
||||
return rio.Text("\\n".join(self.list))
|
||||
|
||||
class ListDemo(rio.Component):
|
||||
list: rio.List[str] = rio.List()
|
||||
|
||||
def build(self):
|
||||
return rio.Column(
|
||||
ElementAdder(self.list),
|
||||
Display(self.list),
|
||||
)
|
||||
|
||||
async with rio.testing.DummyClient(ListDemo) as client:
|
||||
list_demo = client.get_component(ListDemo)
|
||||
display = client.get_component(Display)
|
||||
|
||||
client._received_messages.clear()
|
||||
list_demo.list.append("foo")
|
||||
await client.wait_for_refresh()
|
||||
|
||||
assert display in client._last_updated_components
|
||||
|
||||
|
||||
async def test_dataclass():
|
||||
class Person(rio.Dataclass):
|
||||
name: str
|
||||
|
||||
bob = Person("Bob")
|
||||
|
||||
async with rio.testing.DummyClient(lambda: rio.Text(bob.name)) as client:
|
||||
text_component = client.get_component(rio.Text)
|
||||
|
||||
bob.name = "Bobby"
|
||||
await client.wait_for_refresh()
|
||||
assert text_component.text == "Bobby"
|
||||
|
||||
|
||||
async def test_dataclass_attribute_binding_with_component():
|
||||
class Person(rio.Dataclass):
|
||||
name: str
|
||||
|
||||
class NameChanger(rio.Component):
|
||||
person: Person
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
return rio.TextInput(
|
||||
# Thanks to the attribute binding, typing in the TextInput
|
||||
# will also update the person's name
|
||||
self.person.bind().name
|
||||
)
|
||||
|
||||
bob = Person("Bob")
|
||||
|
||||
async with rio.testing.DummyClient(lambda: NameChanger(bob)) as client:
|
||||
text_input = client.get_component(rio.TextInput)
|
||||
|
||||
text_input.text = "Alice"
|
||||
assert bob.name == "Alice"
|
||||
|
||||
bob.name = "Bob"
|
||||
await client.wait_for_refresh()
|
||||
assert text_input.text == "Bob"
|
||||
|
||||
|
||||
async def test_dataclass_attribute_binding_with_other_dataclass():
|
||||
class Person(rio.Dataclass):
|
||||
name: str
|
||||
|
||||
class Dog(rio.Dataclass):
|
||||
owner: str
|
||||
|
||||
bob = Person("Bob")
|
||||
dog = Dog(bob.bind().name)
|
||||
|
||||
dog.owner = "Alice"
|
||||
assert bob.name == "Alice"
|
||||
|
||||
Reference in New Issue
Block a user