improve docs and add tests

This commit is contained in:
Aran-Fey
2025-12-16 07:34:27 +01:00
parent 54d14d3b82
commit 2ffbe97d45
9 changed files with 188 additions and 29 deletions

View File

@@ -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();
}

View File

@@ -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:

View File

@@ -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",
)
```

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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]

View File

@@ -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"