several small fixes

This commit is contained in:
Jakob Pinterits
2025-11-16 10:43:34 +01:00
parent 9c924ecec5
commit 069831322e
6 changed files with 395 additions and 392 deletions

View File

@@ -96,7 +96,7 @@ Additions:
`rio.Component`.
- Page rework
- Add `rio.Redirect`
- TODO: Automatic page scan
- Still missing automatic page scan
- New experimental `rio.FilePickerArea` component
## 0.9.2

View File

@@ -1,371 +1,371 @@
from __future__ import annotations
import collections.abc
import sys
import typing as t
import rio
from .. import global_state
__all__ = ["List", "Dict", "Set"]
T = t.TypeVar("T")
def identity(x: T) -> T:
return x
class ObservableContainer:
def __init__(self):
self._affected_sessions: set[rio.Session] = set()
def _mark_as_accessed(self):
if global_state.currently_building_session is None:
return
global_state.accessed_objects.add(self)
self._affected_sessions.add(global_state.currently_building_session)
def _mark_as_changed(self) -> None:
for session in self._affected_sessions:
session._changed_objects.add(self)
session._refresh_required_event.set()
class List(ObservableContainer, collections.abc.MutableSequence[T]):
"""
A `list`-like object that automatically rebuilds components whenever its
content changes.
## Examples
```python
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),
)
```
Here you can see how the `Display` component automatically updates whenever
the `ElementAdder` component appends a new element to the `List`. This would
be much trickier to do with a regular `list`.
## Metadata
`experimental`: True
"""
def __init__(self, items: t.Iterable[T] = (), /):
super().__init__()
self._items = list(items)
def insert(self, index: int, value: T) -> None:
self._items.insert(index, value)
self._mark_as_changed()
def append(self, value: T) -> None:
self._items.append(value)
self._mark_as_changed()
def extend(self, values: t.Iterable[T]) -> None:
self._items.extend(values)
self._mark_as_changed()
def remove(self, value: T) -> None:
self._items.remove(value)
self._mark_as_changed()
def clear(self) -> None:
self._items.clear()
self._mark_as_changed()
def pop(self, index: int | None = None, /) -> T:
self._mark_as_accessed()
if index is None:
item = self._items.pop()
else:
item = self._items.pop(index)
self._mark_as_changed()
return item
def reverse(self) -> None:
self._items.reverse()
self._mark_as_changed()
def copy(self) -> List[T]:
self._mark_as_accessed()
return List(self)
def __delitem__(self, index: int | slice) -> None:
del self._items[index]
self._mark_as_changed()
def __add__(self, other: t.Iterable[T], /) -> List[T]:
result = List(self)
result += other
return result
def __iadd__(self, other: t.Iterable[T]) -> List[T]:
self.extend(other)
return self
def index(self, value: T, start: int = 0, stop: int = sys.maxsize) -> int:
self._mark_as_accessed()
return self._items.index(value, start, stop)
def count(self, value: T) -> int:
self._mark_as_accessed()
return self._items.count(value)
@t.overload
def __getitem__(self, index: int) -> T: ...
@t.overload
def __getitem__(self, index: slice) -> List[T]: ...
def __getitem__(self, index: int | slice) -> T | List[T]:
self._mark_as_accessed()
if isinstance(index, slice):
return List(self._items[index])
else:
return self._items[index]
def __len__(self) -> int:
self._mark_as_accessed()
return len(self._items)
def __iter__(self) -> t.Iterator[T]:
# TODO: Technically, no data has been accessed yet. The correct behavior
# would be to return a custom iterator that tracks access in its
# `__next__` method.
self._mark_as_accessed()
return iter(self._items)
def __contains__(self, value: object) -> bool:
self._mark_as_accessed()
return value in self._items
# These function signatures are a PITA. Screw the boilerplate, just inherit
# the signature
if t.TYPE_CHECKING:
__setitem__ = collections.abc.MutableSequence.__setitem__
else:
def sort(self, *args, **kwargs) -> None:
self._items.sort(*args, **kwargs)
self._mark_as_changed()
def __setitem__(self, index_or_slice, value) -> None:
self._items[index_or_slice] = value
self._mark_as_changed()
K = t.TypeVar("K")
V = t.TypeVar("V")
class Dict(ObservableContainer, collections.abc.MutableMapping[K, V]):
"""
A `dict`-like object that automatically rebuilds components whenever its
content changes.
## Examples
```python
class ElementAdder(rio.Component):
dict: rio.Dict[int, str]
def _add_element(self):
self.dict[len(self.dict)] = 'foo'
def build(self):
return rio.Button(
"add an element",
on_press=self._add_element,
)
class Display(rio.Component):
dict: rio.Dict[int, str]
def build(self):
return rio.Text("\n".join(f"{k}: {v}" for k, v in self.dict.items()))
class DictDemo(rio.Component):
dict: rio.Dict[int, str] = rio.Dict()
def build(self):
return rio.Column(
ElementAdder(self.dict),
Display(self.dict),
)
```
Here you can see how the `Display` component automatically updates whenever
the `ElementAdder` component adds a new element to the `Dict`. This would be
much trickier to do with a regular `dict`.
## Metadata
`experimental`: True
"""
def __init__(
self,
__items: t.Mapping[K, V] | t.Iterable[tuple[K, V]] = (),
/,
**kwargs: V,
):
super().__init__()
self._items = dict(__items, **kwargs)
def __setitem__(self, key: K, value: V, /) -> None:
self._items[key] = value
self._mark_as_changed()
def __delitem__(self, key: K, /) -> None:
del self._items[key]
self._mark_as_changed()
def __getitem__(self, key: K, /) -> V:
self._mark_as_accessed()
return self._items[key]
def __iter__(self) -> t.Iterator[K]:
self._mark_as_accessed()
return iter(self._items)
def __len__(self) -> int:
self._mark_as_accessed()
return len(self._items)
def __contains__(self, key: object, /) -> bool:
self._mark_as_accessed()
return key in self._items
def popitem(self) -> tuple[K, V]:
self._mark_as_accessed()
item = self._items.popitem()
self._mark_as_changed()
return item
# These function signatures are a PITA. Screw the boilerplate, just inherit
# the signature
if not t.TYPE_CHECKING:
def update(self, *args, **kwargs) -> None:
self._items.update(*args, **kwargs)
self._mark_as_changed()
def pop(self, *args, **kwargs):
self._mark_as_accessed()
value = self._items.pop(*args, **kwargs)
self._mark_as_changed()
return value
class Set(ObservableContainer, collections.abc.MutableSet[T]):
"""
A `set`-like object that automatically rebuilds components whenever its
content changes.
## Examples
```python
class ElementAdder(rio.Component):
set: rio.Set[str]
def build(self):
return rio.Button(
"add an element",
on_press=lambda: self.set.add(len(self.set)),
)
class Display(rio.Component):
set: rio.Set[str]
def build(self):
return rio.Text("\n".join(self.set))
class SetDemo(rio.Component):
set: rio.Set[str] = rio.Set()
def build(self):
return rio.Column(
ElementAdder(self.set),
Display(self.set),
)
```
Here you can see how the `Display` component automatically updates whenever
the `ElementAdder` component adds a new element to the `Set`. This would be
much trickier to do with a regular `set`.
## Metadata
`experimental`: True
"""
def __init__(self, items: t.Iterable[T] = (), /):
super().__init__()
self._items = set(items)
def __iter__(self) -> t.Iterator[T]:
self._mark_as_accessed()
return iter(self._items)
def __len__(self) -> int:
self._mark_as_accessed()
return len(self._items)
def __contains__(self, value: object) -> bool:
self._mark_as_accessed()
return value in self._items
def add(self, value: T) -> None:
self._items.add(value)
self._mark_as_changed()
def update(self, values: t.Iterable[T]) -> None:
self._items.update(values)
self._mark_as_changed()
def discard(self, value: T) -> None:
self._items.discard(value)
self._mark_as_changed()
def clear(self) -> None:
self._items.clear()
self._mark_as_changed()
from __future__ import annotations
import collections.abc
import sys
import typing as t
import rio
from .. import global_state
__all__ = ["List", "Dict", "Set"]
T = t.TypeVar("T")
def identity(x: T) -> T:
return x
class ObservableContainer:
def __init__(self):
self._affected_sessions: set[rio.Session] = set()
def _mark_as_accessed(self):
if global_state.currently_building_session is None:
return
global_state.accessed_objects.add(self)
self._affected_sessions.add(global_state.currently_building_session)
def _mark_as_changed(self) -> None:
for session in self._affected_sessions:
session._changed_objects.add(self)
session._refresh_required_event.set()
class List(ObservableContainer, collections.abc.MutableSequence[T]):
"""
A `list`-like object that automatically rebuilds components whenever its
content changes.
## Examples
```python
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),
)
```
Here you can see how the `Display` component automatically updates whenever
the `ElementAdder` component appends a new element to the `List`. This would
be much trickier to do with a regular `list`.
## Metadata
`experimental`: True
"""
def __init__(self, items: t.Iterable[T] = (), /):
super().__init__()
self._items = list(items)
def insert(self, index: int, value: T) -> None:
self._items.insert(index, value)
self._mark_as_changed()
def append(self, value: T) -> None:
self._items.append(value)
self._mark_as_changed()
def extend(self, values: t.Iterable[T]) -> None:
self._items.extend(values)
self._mark_as_changed()
def remove(self, value: T) -> None:
self._items.remove(value)
self._mark_as_changed()
def clear(self) -> None:
self._items.clear()
self._mark_as_changed()
def pop(self, index: int | None = None, /) -> T:
self._mark_as_accessed()
if index is None:
item = self._items.pop()
else:
item = self._items.pop(index)
self._mark_as_changed()
return item
def reverse(self) -> None:
self._items.reverse()
self._mark_as_changed()
def copy(self) -> List[T]:
self._mark_as_accessed()
return List(self)
def __delitem__(self, index: int | slice) -> None:
del self._items[index]
self._mark_as_changed()
def __add__(self, other: t.Iterable[T], /) -> List[T]:
result = List(self)
result += other
return result
def __iadd__(self, other: t.Iterable[T]) -> List[T]:
self.extend(other)
return self
def index(self, value: T, start: int = 0, stop: int = sys.maxsize) -> int:
self._mark_as_accessed()
return self._items.index(value, start, stop)
def count(self, value: T) -> int:
self._mark_as_accessed()
return self._items.count(value)
@t.overload
def __getitem__(self, index: int) -> T: ...
@t.overload
def __getitem__(self, index: slice) -> List[T]: ...
def __getitem__(self, index: int | slice) -> T | List[T]:
self._mark_as_accessed()
if isinstance(index, slice):
return List(self._items[index])
else:
return self._items[index]
def __len__(self) -> int:
self._mark_as_accessed()
return len(self._items)
def __iter__(self) -> t.Iterator[T]:
# TODO: Technically, no data has been accessed yet. The correct behavior
# would be to return a custom iterator that tracks access in its
# `__next__` method.
self._mark_as_accessed()
return iter(self._items)
def __contains__(self, value: object) -> bool:
self._mark_as_accessed()
return value in self._items
# These function signatures are a PITA. Screw the boilerplate, just inherit
# the signature
if t.TYPE_CHECKING:
__setitem__ = collections.abc.MutableSequence.__setitem__
else:
def sort(self, *args, **kwargs) -> None:
self._items.sort(*args, **kwargs)
self._mark_as_changed()
def __setitem__(self, index_or_slice, value) -> None:
self._items[index_or_slice] = value
self._mark_as_changed()
K = t.TypeVar("K")
V = t.TypeVar("V")
class Dict(ObservableContainer, collections.abc.MutableMapping[K, V]):
"""
A `dict`-like object that automatically rebuilds components whenever its
content changes.
## Examples
```python
class ElementAdder(rio.Component):
dict: rio.Dict[int, str]
def _add_element(self):
self.dict[len(self.dict)] = "foo"
def build(self):
return rio.Button(
"add an element",
on_press=self._add_element,
)
class Display(rio.Component):
dict: rio.Dict[int, str]
def build(self):
return rio.Text("\\n".join(f"{k}: {v}" for k, v in self.dict.items()))
class DictDemo(rio.Component):
dict: rio.Dict[int, str] = rio.Dict()
def build(self):
return rio.Column(
ElementAdder(self.dict),
Display(self.dict),
)
```
Here you can see how the `Display` component automatically updates whenever
the `ElementAdder` component adds a new element to the `Dict`. This would be
much trickier to do with a regular `dict`.
## Metadata
`experimental`: True
"""
def __init__(
self,
__items: t.Mapping[K, V] | t.Iterable[tuple[K, V]] = (),
/,
**kwargs: V,
):
super().__init__()
self._items = dict(__items, **kwargs)
def __setitem__(self, key: K, value: V, /) -> None:
self._items[key] = value
self._mark_as_changed()
def __delitem__(self, key: K, /) -> None:
del self._items[key]
self._mark_as_changed()
def __getitem__(self, key: K, /) -> V:
self._mark_as_accessed()
return self._items[key]
def __iter__(self) -> t.Iterator[K]:
self._mark_as_accessed()
return iter(self._items)
def __len__(self) -> int:
self._mark_as_accessed()
return len(self._items)
def __contains__(self, key: object, /) -> bool:
self._mark_as_accessed()
return key in self._items
def popitem(self) -> tuple[K, V]:
self._mark_as_accessed()
item = self._items.popitem()
self._mark_as_changed()
return item
# These function signatures are a PITA. Screw the boilerplate, just inherit
# the signature
if not t.TYPE_CHECKING:
def update(self, *args, **kwargs) -> None:
self._items.update(*args, **kwargs)
self._mark_as_changed()
def pop(self, *args, **kwargs):
self._mark_as_accessed()
value = self._items.pop(*args, **kwargs)
self._mark_as_changed()
return value
class Set(ObservableContainer, collections.abc.MutableSet[T]):
"""
A `set`-like object that automatically rebuilds components whenever its
content changes.
## Examples
```python
class ElementAdder(rio.Component):
set: rio.Set[str]
def build(self):
return rio.Button(
"add an element",
on_press=lambda: self.set.add(len(self.set)),
)
class Display(rio.Component):
set: rio.Set[str]
def build(self):
return rio.Text("\\n".join(self.set))
class SetDemo(rio.Component):
set: rio.Set[str] = rio.Set()
def build(self):
return rio.Column(
ElementAdder(self.set),
Display(self.set),
)
```
Here you can see how the `Display` component automatically updates whenever
the `ElementAdder` component adds a new element to the `Set`. This would be
much trickier to do with a regular `set`.
## Metadata
`experimental`: True
"""
def __init__(self, items: t.Iterable[T] = (), /):
super().__init__()
self._items = set(items)
def __iter__(self) -> t.Iterator[T]:
self._mark_as_accessed()
return iter(self._items)
def __len__(self) -> int:
self._mark_as_accessed()
return len(self._items)
def __contains__(self, value: object) -> bool:
self._mark_as_accessed()
return value in self._items
def add(self, value: T) -> None:
self._items.add(value)
self._mark_as_changed()
def update(self, values: t.Iterable[T]) -> None:
self._items.update(values)
self._mark_as_changed()
def discard(self, value: T) -> None:
self._items.discard(value)
self._mark_as_changed()
def clear(self) -> None:
self._items.clear()
self._mark_as_changed()

View File

@@ -1936,6 +1936,16 @@ a.remove();
@property
def theme(self) -> theme.Theme:
"""
The theme currently used by this session.
If you've passed both a light and dark theme to your app, this will be
whichever one is actually being used by the client. You can also assign
a new theme to this property to change the theme for this session.
Note that changing the theme will only affect this specific session, not
the entire app.
"""
return self._theme
@theme.setter
@@ -3121,6 +3131,7 @@ a.remove();
properties_to_serialize: IdentityDefaultDict[object, set[str]],
):
components_to_build = set[rio.Component]()
permanent_component_level_cache: dict[rio.Component, int] = {}
while True:
# Update the properties_to_serialize
@@ -3135,19 +3146,8 @@ a.remove();
self._changed_items.clear()
self._refresh_required_event.clear()
# We need to build parents before children, but some components
# haven't had their `_weak_parent_` set yet, so we don't know who
# their parent is. We need to find the topmost components and build
# them.
# TODO: This is not entirely correct, because during the build
# process, new components can be instantiated or the level of an
# existing component can change. The correct solution would be to
# process one component, then call `_collect_components_to_build()`
# again, and sort again. TODO: I think this *is* correct now that we
# use parents instead of builders
component_level_cache = {}
# Find the topmost components and build them
component_level_cache = permanent_component_level_cache.copy()
components_by_level = collections.defaultdict[
int, list[rio.Component]
](list)
@@ -3185,6 +3185,9 @@ a.remove();
yield from components_to_build_in_this_iteration
for component in components_to_build_in_this_iteration:
permanent_component_level_cache[component] = level_to_build
async def _refresh(self) -> None:
"""
Make sure the session state is up to date. Specifically:
@@ -4066,9 +4069,9 @@ def find_components_for_reconciliation(
yield old_component, new_component
# Compare the children, but make sure to preserve the topology.
# Can't just use `iter_direct_children` here, since that would
# discard topological information.
# Compare the children, but make sure to preserve the topology. Can't
# just use `iter_direct_children` here, since that would discard
# topological information.
#
# Also, in this context, "children" means *only* "components stored in
# attributes", *not* "tree children". Reconciliation is about component

View File

@@ -177,7 +177,7 @@ class BrowserClient(BaseClient):
marker.style.top = `{y}px`;
marker.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(marker);
setTimeout(() => {{
marker.remove();
}}, {sleep} * 1000);

View File

@@ -58,10 +58,10 @@ async def test_3rd_party_elements_dont_affect_layout(
)
assert user_content_height == pytest.approx(
await client.get_window_height()
await client.get_window_height(), abs=0.1
)
dev_tools_sidebar_width = await client.get_dev_tools_sidebar_width()
assert user_content_width == pytest.approx(
await client.get_window_width() - dev_tools_sidebar_width
await client.get_window_width() - dev_tools_sidebar_width, abs=0.1
)

View File

@@ -3,7 +3,7 @@
"target": "es2022",
"module": "es2022",
"lib": ["es2021", "dom", "dom.iterable"],
"strict": false, // TODO: fix and enable strict checking
"strict": true,
"moduleResolution": "node",
"skipLibCheck": true,
"useDefineForClassFields": false