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.2k stars 773 forks source link

Crash on pushing screen when a toast is active #4607

Closed darrenburns closed 3 months ago

darrenburns commented 3 months ago

I haven't been able to isolate an MRE for this. It works if I clear the notifications before I push my modal screen.

MountError: Can't mount widget(s) before
ToastHolder(id='--textual-toast-8456776f-9c7c-49ab-a3b8-cdfcfdcc6d85') is mounted

More details below:

Details
╭─────────────────────── Traceback (most recent call last) ────────────────────────╮
│ /Users/darrenburns/Code/posting/.venv/lib/python3.11/site-packages/textual/widge │
│ t.py:3720 in _compose                                                            │
│                                                                                  │
│   3717 │   │   │   self.app._handle_exception(error)                             │
│   3718 │   │   else:                                                             │
│   3719 │   │   │   self._extend_compose(widgets)                                 │
│ ❱ 3720 │   │   │   await self.mount_composed_widgets(widgets)                    │
│   3721 │                                                                         │
│   3722 │   async def mount_composed_widgets(self, widgets: list[Widget]) -> None │
│   3723 │   │   """Called by Textual to mount widgets after compose.              │
│                                                                                  │
│ ╭─────────────────────────────────── locals ───────────────────────────────────╮ │
│ │    self = ToastHolder(                                                       │ │
│ │           │   id='--textual-toast-8456776f-9c7c-49ab-a3b8-cdfcfdcc6d85'      │ │
│ │           )                                                                  │ │
│ │ widgets = [Toast()]                                                          │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│ /Users/darrenburns/Code/posting/.venv/lib/python3.11/site-packages/textual/widge │
│ t.py:3733 in mount_composed_widgets                                              │
│                                                                                  │
│   3730 │   │   │   widgets: A list of child widgets.                             │
│   3731 │   │   """                                                               │
│   3732 │   │   if widgets:                                                       │
│ ❱ 3733 │   │   │   await self.mount_all(widgets)                                 │
│   3734 │                                                                         │
│   3735 │   def _extend_compose(self, widgets: list[Widget]) -> None:             │
│   3736 │   │   """Hook to extend composed widgets.                               │
│                                                                                  │
│ ╭─────────────────────────────────── locals ───────────────────────────────────╮ │
│ │    self = ToastHolder(                                                       │ │
│ │           │   id='--textual-toast-8456776f-9c7c-49ab-a3b8-cdfcfdcc6d85'      │ │
│ │           )                                                                  │ │
│ │ widgets = [Toast()]                                                          │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│ /Users/darrenburns/Code/posting/.venv/lib/python3.11/site-packages/textual/widge │
│ t.py:1007 in mount_all                                                           │
│                                                                                  │
│   1004 │   │   │   Only one of ``before`` or ``after`` can be provided. If both  │
│   1005 │   │   │   provided a ``MountError`` will be raised.                     │
│   1006 │   │   """                                                               │
│ ❱ 1007 │   │   await_mount = self.mount(*widgets, before=before, after=after)    │
│   1008 │   │   return await_mount                                                │
│   1009 │                                                                         │
│   1010 │   @overload                                                             │
│                                                                                  │
│ ╭─────────────────────────────────── locals ───────────────────────────────────╮ │
│ │   after = None                                                               │ │
│ │  before = None                                                               │ │
│ │    self = ToastHolder(                                                       │ │
│ │           │   id='--textual-toast-8456776f-9c7c-49ab-a3b8-cdfcfdcc6d85'      │ │
│ │           )                                                                  │ │
│ │ widgets = [Toast()]                                                          │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│ /Users/darrenburns/Code/posting/.venv/lib/python3.11/site-packages/textual/widge │
│ t.py:940 in mount                                                                │
│                                                                                  │
│    937 │   │   if self._closing:                                                 │
│    938 │   │   │   return AwaitMount(self, [])                                   │
│    939 │   │   if not self.is_attached:                                          │
│ ❱  940 │   │   │   raise MountError(f"Can't mount widget(s) before {self!r} is m │
│    941 │   │   # Check for duplicate IDs in the incoming widgets                 │
│    942 │   │   ids_to_mount = [                                                  │
│    943 │   │   │   widget_id for widget in widgets if (widget_id := widget.id) i │
│                                                                                  │
│ ╭─────────────────────────────────── locals ───────────────────────────────────╮ │
│ │   after = None                                                               │ │
│ │  before = None                                                               │ │
│ │    self = ToastHolder(                                                       │ │
│ │           │   id='--textual-toast-8456776f-9c7c-49ab-a3b8-cdfcfdcc6d85'      │ │
│ │           )                                                                  │ │
│ │ widgets = (Toast(),)                                                         │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────╯
MountError: Can't mount widget(s) before
ToastHolder(id='--textual-toast-8456776f-9c7c-49ab-a3b8-cdfcfdcc6d85') is mounted
darrenburns commented 3 months ago

I was calling self.recompose inside on_resize inside the Screen I was pushing. This fails if there's a notification on screen as the first resize event arrives too early. I just put a block in to prevent it running on the first resize event and it works as expect.

willmcgugan commented 3 months ago

I suspect that is down to the call to self.app._refresh_notifications() in on_screen_resume. If that was awaited, it could fix it.

Could you try for an MRE again? I suspect it will be a matter of just repeating things very fast.

darrenburns commented 3 months ago

Press n and then p:

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import ModalScreen
from textual.widgets import Footer, Label

class MyScreen(ModalScreen[None]):
    def compose(self) -> ComposeResult:
        yield Label("MyScreen")

    async def on_resize(self) -> None:
        await self.recompose()

class MountCrash(App[None]):
    BINDINGS = [
        Binding("p", "push", "Push a new screen"),
        Binding("n", "notification", "Notification"),
    ]

    def compose(self) -> ComposeResult:
        yield Label("App")
        yield Footer()

    def action_notification(self) -> None:
        self.notify("Hello, world!", title="Notification message", timeout=10)

    async def action_push(self) -> None:
        await self.push_screen(MyScreen(), callback=lambda _: None)

app = MountCrash()
if __name__ == "__main__":
    app.run()
mzebrak commented 3 months ago

I think that's related to: https://github.com/Textualize/textual/issues/4570 (see the last MRE of my last response - it also happens on screen change, but pop_screen in my case)

github-actions[bot] commented 3 months ago

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

mzebrak commented 3 months ago

Unfortunately, the latest change in locks seems does not fix any of my issues mentioned in the linked (closed) issue #4570.

Could I ask you @darrenburns to take a look at this, please? Maybe you'll notice something right away, and I have some problems shown there (in MREs), like this mounting error and even the application freeze.

willmcgugan commented 3 months ago

Please do not comment on closed issues. We need an open issue to track tasks.

There's no need to mention any particular dev.