self.watch
causes asynchronous callbacks to run synchronously
#3171
-
Might be related to: #3036 Correct me if I'm wrong, but it seems to me that the from __future__ import annotations
import asyncio
from inspect import isawaitable
from time import perf_counter
from typing import TYPE_CHECKING, Any
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.reactive import var
from textual.widget import Widget
from textual.widgets import Footer, Label, LoadingIndicator
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from textual.reactive import Reactable
class ReactiveHolder(Widget):
DEFAULT_CSS = """
ReactiveHolder {
height: 0;
width: 0;
}
"""
trigger = var(False, init=False)
class DynamicLabel(Widget):
"""A label that can be updated dynamically when a reactive variable changes."""
DEFAULT_CSS = """
DynamicLabel {
height: auto;
}
DynamicLabel LoadingIndicator {
height: 1;
}
"""
def __init__(
self,
obj_to_watch: Reactable,
attribute_name: str,
callback: Callable[[Any], str] | Callable[[Any], Awaitable[str]],
) -> None:
super().__init__()
self.__label = Label("loading...")
self.__label.display = False
self.__loading_indicator = LoadingIndicator()
self.__obj_to_watch = obj_to_watch
self.__attribute_name = attribute_name
self.__callback = callback
async def on_mount(self) -> None:
self.watch(self.__obj_to_watch, self.__attribute_name, self.attribute_changed, init=False)
def compose(self) -> ComposeResult:
yield self.__loading_indicator
yield self.__label
async def attribute_changed(self, attribute: Any) -> None:
await asyncio.sleep(1) # Stimulate callback takes a long time to complete
value = self.__callback(attribute)
if isawaitable(value):
value = await value
self.__label.update(f"{value}")
self.__loading_done()
def __loading_done(self) -> None:
if self.__loading_indicator.display:
self.__loading_indicator.display = False
self.__label.display = True
class MyApp(App):
BINDINGS = [
Binding("l", "load_data", "Load data"),
]
def __init__(self) -> None:
super().__init__()
self.reactive_holder = ReactiveHolder()
def on_mount(self) -> None:
self.mount(self.reactive_holder)
def action_load_data(self) -> None:
self.button_pressed_time = perf_counter()
self.reactive_holder.trigger = not self.reactive_holder.trigger
def compose(self) -> ComposeResult:
yield DynamicLabel(self.reactive_holder, "trigger", self.load_data)
yield DynamicLabel(self.reactive_holder, "trigger", self.load_data)
yield DynamicLabel(self.reactive_holder, "trigger", self.load_data)
yield Footer()
async def load_data(self, _: bool) -> str:
return f"Loading took {perf_counter() - self.button_pressed_time:.2f} seconds"
if __name__ == "__main__":
MyApp().run() After pressing While the correct behaviour should be: And it works like this if we wrap the callback in async def on_mount(self) -> None:
def delegate_work(attribute: Any) -> None:
self.run_worker(self.attribute_changed(attribute))
self.watch(self.__obj_to_watch, self.__attribute_name, delegate_work, init=False) |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 9 replies
-
Thank you for your issue. Give us a little time to review it. PS. You might want to check the FAQ if you haven't done so already. This is an automated reply, generated by FAQtory |
Beta Was this translation helpful? Give feedback.
-
Additionally, maybe someone would have suggestions on how to implement such a |
Beta Was this translation helpful? Give feedback.
-
All callbacks are serialized with the widget's message queue. You almost always want this. If Textual launched tasks for such things, it would make it next to impossible to predict the state of your app from one call to the next. If you do want concurrency, you can use Textual's Workers.
CSS types in Textual will work down the widget's MRO. So the CSS from the base class still applies.
I don't understand this. Perhaps you can rephrase? |
Beta Was this translation helpful? Give feedback.
All callbacks are serialized with the widget's message queue. You almost always want this. If Textual launched tasks for such things, it would make it next to impossible to predict the state of your app from one call to the next. If you do want concurrency, you can use Textual's Workers.
CSS types in Textual will work down the widget's MRO. So the CSS from the base class still applies.