Textualize / textual

The lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.
https://textual.textualize.io/
MIT License
25.76k stars 795 forks source link

LoadingIndicator stopped working from 0.80.2+ #5262

Closed yorevs closed 6 days ago

yorevs commented 6 days ago

Hello all,

After version 0.80.1 the LoadingIndicator of an Input component stopped working. This is the traceback:

/opt/homebrew/lib/python3.11/site-packages/textual/worker.py:368 in _run                                                                                                                                                                 │
│                                                                                                                                                                                                                                          │
│   365 │   │   │   self.state = WorkerState.RUNNING                                             ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮                      │
│   366 │   │   │   app.log.worker(self)                                                         │           app = AskAiApp(title='AskAiApp', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'})             │                      │
│   367 │   │   │   try:                                                                         │         error = RuntimeError('no running event loop')                                                            │                      │
│ ❱ 368 │   │   │   │   self._result = await self.run()                                          │          self = <Worker ERROR name='ask_and_reply' description="ask_and_reply('what is the size of the Moon?')"> │                      │
│   369 │   │   │   except asyncio.CancelledError as error:                                      │     Traceback = <class 'rich.traceback.Traceback'>                                                               │                      │
│   370 │   │   │   │   self.state = WorkerState.CANCELLED                                       │ worker_failed = WorkerFailed("Worker raised exception: RuntimeError('no running event loop')")                   │                      │
│   371 │   │   │   │   self._error = error                                                      ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                      │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/worker.py:352 in run                                                                                                                                                                  │
│                                                                                                                                                                                                                                          │
│   349 │   │   Returns:                                                                         ╭──────────────────────────────────────────────── locals ─────────────────────────────────────────────────╮                               │
│   350 │   │   │   Return value of the work.                                                    │ self = <Worker ERROR name='ask_and_reply' description="ask_and_reply('what is the size of the Moon?')"> │                               │
│   351 │   │   """                                                                              ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯                               │
│ ❱ 352 │   │   return await (                                                                                                                                                                                                             │
│   353 │   │   │   self._run_threaded() if self._thread_worker else self._run_async()                                                                                                                                                     │
│   354 │   │   )                                                                                                                                                                                                                          │
│   355                                                                                                                                                                                                                                    │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/worker.py:324 in _run_threaded                                                                                                                                                        │
│                                                                                                                                                                                                                                          │
│   321 │   │                                                                                    ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮                      │
│   322 │   │   loop = asyncio.get_running_loop()                                                │          loop = <_UnixSelectorEventLoop running=True closed=False debug=False>                                   │                      │
│   323 │   │   assert loop is not None                                                          │ run_awaitable = <function Worker._run_threaded.<locals>.run_awaitable at 0x375c4a700>                            │                      │
│ ❱ 324 │   │   return await loop.run_in_executor(None, runner, self._work)                      │  run_callable = <function Worker._run_threaded.<locals>.run_callable at 0x375c4b600>                             │                      │
│   325 │                                                                                        │ run_coroutine = <function Worker._run_threaded.<locals>.run_coroutine at 0x375c49620>                            │                      │
│   326 │   async def _run_async(self) -> ResultType:                                            │        runner = <function Worker._run_threaded.<locals>.run_callable at 0x375c4b600>                             │                      │
│   327 │   │   """Run an async worker.                                                          │          self = <Worker ERROR name='ask_and_reply' description="ask_and_reply('what is the size of the Moon?')"> │                      │
│                                                                                                ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                      │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/Cellar/python@3.11/3.11.9_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/concurrent/futures/thread.py:58 in run                                                                                                │
│                                                                                                                                                                                                                                          │
│    55 │   │   │   return                                                                       ╭── locals ───╮                                                                                                                           │
│    56 │   │                                                                                    │ self = None │                                                                                                                           │
│    57 │   │   try:                                                                             ╰─────────────╯                                                                                                                           │
│ ❱  58 │   │   │   result = self.fn(*self.args, **self.kwargs)                                                                                                                                                                            │
│    59 │   │   except BaseException as exc:                                                                                                                                                                                               │
│    60 │   │   │   self.future.set_exception(exc)                                                                                                                                                                                         │
│    61 │   │   │   # Break a reference cycle with the exception 'exc'                                                                                                                                                                     │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/worker.py:307 in run_callable                                                                                                                                                         │
│                                                                                                                                                                                                                                          │
│   304 │   │   def run_callable(work: Callable[[], ResultType]) -> ResultType:                                                                                                                                                            │
│   305 │   │   │   """Set the active worker, and call the callable."""                                                                                                                                                                    │
│   306 │   │   │   active_worker.set(self)                                                                                                                                                                                                │
│ ❱ 307 │   │   │   return work()                                                                                                                                                                                                          │
│   308 │   │                                                                                                                                                                                                                              │
│   309 │   │   if (                                                                                                                                                                                                                       │
│   310 │   │   │   inspect.iscoroutinefunction(self._work)                                                                                                                                                                                │
│                                                                                                                                                                                                                                          │
│ ╭───────────────────────────────────────────────────────────────────────────────────────────── locals ──────────────────────────────────────────────────────────────────────────────────────────────╮                                    │
│ │ self = <Worker ERROR name='ask_and_reply' description="ask_and_reply('what is the size of the Moon?')">                                                                                           │                                    │
│ │ work = functools.partial(<function AskAiApp.ask_and_reply at 0x36e1fb880>, AskAiApp(title='AskAiApp', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}), 'what is the size of the Moon?') │                                    │
│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                    │
│                                                                                                                                                                                                                                          │
│ /Users/hjunior/GIT-Repository/GitHub/askai/src/main/askai/tui/askai_app.py:373 in ask_and_reply                                                                                                                                          │
│                                                                                                                                                                                                                                          │
│   370 │   │   :param question: The question to ask the AI engine.                              ╭──────────────────────────────────────────── locals ─────────────────────────────────────────────╮                                       │
│   371 │   │   :return: A tuple containing a boolean indicating success or failure, and the AI' │ question = 'what is the size of the Moon?'                                                      │                                       │
│   372 │   │   """                                                                              │     self = AskAiApp(title='AskAiApp', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) │                                       │
│ ❱ 373 │   │   self.enable_controls(False)                                                      ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯                                       │
│   374 │   │   status, reply = self.askai.ask_and_reply(question)                                                                                                                                                                         │
│   375 │   │   self.enable_controls()                                                                                                                                                                                                     │
│   376                                                                                                                                                                                                                                    │
│                                                                                                                                                                                                                                          │
│ /Users/hjunior/GIT-Repository/GitHub/askai/src/main/askai/tui/askai_app.py:212 in enable_controls                                                                                                                                        │
│                                                                                                                                                                                                                                          │
│   209 │   │   :param enable: Whether to enable (True) or disable (False) the UI controls (defa ╭─────────────────────────────────────────── locals ────────────────────────────────────────────╮                                         │
│   210 │   │   """                                                                              │ enable = False                                                                                │                                         │
│   211 │   │   self.header.disabled = not enable                                                │   self = AskAiApp(title='AskAiApp', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) │                                         │
│ ❱ 212 │   │   self.line_input.loading = not enable                                             ╰───────────────────────────────────────────────────────────────────────────────────────────────╯                                         │
│   213 │   │   self.footer.disabled = not enable                                                                                                                                                                                          │
│   214 │                                                                                                                                                                                                                                  │
│   215 │   def activate_markdown(self) -> None:                                                                                                                                                                                           │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/widget.py:905 in _watch_loading                                                                                                                                                       │
│                                                                                                                                                                                                                                          │
│    902 │                                                                                        ╭───── locals ──────╮                                                                                                                    │
│    903 │   def _watch_loading(self, loading: bool) -> None:                                     │ loading = True    │                                                                                                                    │
│    904 │   │   """Called when the 'loading' reactive is changed."""                             │    self = Input() │                                                                                                                    │
│ ❱  905 │   │   self.set_loading(loading)                                                        ╰───────────────────╯                                                                                                                    │
│    906 │                                                                                                                                                                                                                                 │
│    907 │   ExpectType = TypeVar("ExpectType", bound="Widget")                                                                                                                                                                            │
│    908                                                                                                                                                                                                                                   │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/widget.py:899 in set_loading                                                                                                                                                          │
│                                                                                                                                                                                                                                          │
│    896 │   │   if loading:                                                                      ╭──────────────────────── locals ────────────────────────╮                                                                               │
│    897 │   │   │   loading_indicator = self.get_loading_widget()                                │                 loading = True                         │                                                                               │
│    898 │   │   │   loading_indicator.add_class(LOADING_INDICATOR_CLASS)                         │       loading_indicator = LoadingIndicator()           │                                                                               │
│ ❱  899 │   │   │   self._cover(loading_indicator)                                               │ LOADING_INDICATOR_CLASS = '-textual-loading-indicator' │                                                                               │
│    900 │   │   else:                                                                            │                    self = Input()                      │                                                                               │
│    901 │   │   │   self._uncover()                                                              ╰────────────────────────────────────────────────────────╯                                                                               │
│    902                                                                                                                                                                                                                                   │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/widget.py:648 in _cover                                                                                                                                                               │
│                                                                                                                                                                                                                                          │
│    645 │   │   self._uncover()                                                                  ╭────────── locals ───────────╮                                                                                                          │
│    646 │   │   self._cover_widget = widget                                                      │   self = Input()            │                                                                                                          │
│    647 │   │   widget._parent = self                                                            │ widget = LoadingIndicator() │                                                                                                          │
│ ❱  648 │   │   widget._start_messages()                                                         ╰─────────────────────────────╯                                                                                                          │
│    649 │   │   widget._post_register(self.app)                                                                                                                                                                                           │
│    650 │   │   self.app.stylesheet.apply(widget)                                                                                                                                                                                         │
│    651 │   │   self.refresh(layout=True)                                                                                                                                                                                                 │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/lib/python3.11/site-packages/textual/message_pump.py:500 in _start_messages                                                                                                                                                │
│                                                                                                                                                                                                                                          │
│   497 │   def _start_messages(self) -> None:                                                   ╭───────── locals ──────────╮                                                                                                             │
│   498 │   │   """Start messages task."""                                                       │ self = LoadingIndicator() │                                                                                                             │
│   499 │   │   if self.app._running:                                                            ╰───────────────────────────╯                                                                                                             │
│ ❱ 500 │   │   │   self._task = create_task(                                                                                                                                                                                              │
│   501 │   │   │   │   self._process_messages(), name=f"message pump {self}"                                                                                                                                                              │
│   502 │   │   │   )                                                                                                                                                                                                                      │
│   503 │   │   else:                                                                                                                                                                                                                      │
│                                                                                                                                                                                                                                          │
│ /opt/homebrew/Cellar/python@3.11/3.11.9_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/tasks.py:381 in create_task                                                                                                   │
│                                                                                                                                                                                                                                          │
│   378 │                                                                                        ╭───────────────────────────────── locals ──────────────────────────────────╮                                                             │
│   379 │   Return a Task object.                                                                │ context = None                                                            │                                                             │
│   380 │   """                                                                                  │    coro = <coroutine object MessagePump._process_messages at 0x36e71c130> │                                                             │
│ ❱ 381 │   loop = events.get_running_loop()                                                     │    name = 'message pump LoadingIndicator()'                               │                                                             │
│   382 │   if context is None:                                                                  ╰───────────────────────────────────────────────────────────────────────────╯                                                             │
│   383 │   │   # Use legacy API if context is not needed                                                                                                                                                                                  │
│   384 │   │   task = loop.create_task(coro)                                                                                                                                                                                              │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
RuntimeError: no running event loop

Setting loading = true in another context:

RuntimeError: Task <Task pending name='Task-1' coro=<App.run.<locals>.run_app() running at /opt/homebrew/lib/python3.11/site-packages/textual/app.py:2051> cb=[_run_until_complete_cb() at
/opt/homebrew/Cellar/python@3.11/3.11.9_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_events.py:181]> got Future <_GatheringFuture pending> attached to a different loop

If I rollback to 0.80.1 it works again.

github-actions[bot] commented 6 days ago

We found the following entry in the FAQ which you may find helpful:

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by FAQtory

TomJGooding commented 6 days ago

This sounds similar to https://github.com/Textualize/textual/issues/5137#issuecomment-2421626889, assuming you are using a thread worker?

yorevs commented 6 days ago

textual-loading-indicator-error

This sounds similar to #5137 (comment), assuming you are using a thread worker?

Yes, Indeed I am. this is the function:

@work(thread=True, exclusive=True)
def ask_and_reply(self, question: str) -> tuple[bool, Optional[str]]:
    self.enable_controls(False). # Here I toggle the line_input.loading = true/false
    ...

Similar to that issue, except that it's not inside on_mount. Actually, when it's inside it, it works.

TomJGooding commented 6 days ago

The issue is that most Textual functions are not thread-safe (unrelated to on_mount). Try the suggested workaround in the thread worker docs.

The first difference [with thread workers] is that you should avoid calling methods on your UI directly, or setting reactive variables. You can work around this with the App.call_from_thread method which schedules a call in the main thread.

yorevs commented 6 days ago

The issue is that most Textual functions are not thread-safe (unrelated to on_mount). Try the suggested workaround in the thread worker docs.

The first difference [with thread workers] is that you should avoid calling methods on your UI directly, or setting reactive variables. You can work around this with the App.call_from_thread method which schedules a call in the main thread.

Using call_from_thread solved the problem. Thanks. I will close the issue.

github-actions[bot] commented 6 days ago

Don't forget to star the repository!

Follow @textualizeio for Textual updates.