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
24.07k stars 741 forks source link

"Can't mount widget(s) before Vertical() is mounted" when reactive recompose #4691

Closed comzyh closed 2 days ago

comzyh commented 2 days ago

Similar to #4570.

Reproduce: using the following code and press the "t" key repeatedly.

import asyncio

from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Footer, Header, Label

class FooWidget(Widget):

    num_lines = reactive(0, recompose=True)

    def compose(self) -> ComposeResult:
        with Vertical():
            for i in range(self.num_lines):
                yield Label(f"Test line {i}")

    async def add_widgets(self) -> None:
        for i in range(32):
            self.num_lines += 1
            # v = self.query_one(Vertical)
            # v.mount(Label(f"Test line {i}"))
            await asyncio.sleep(0.01)

class TestApp(App):
    BINDINGS = [
        ("t", "trigger", "Test"),
        ("q", "quit", "Quit"),
    ]

    def compose(self) -> ComposeResult:
        yield Header()

        with Vertical(id="vertical_01"):
            yield FooWidget()
        yield Footer()

    async def action_trigger(self) -> None:

        v = self.query_one("#vertical_01", Vertical)
        f = v.query_one(FooWidget)
        f.workers.cancel_all()
        await f.remove()
        new_f = FooWidget(id="new_f")
        await_mount = v.mount(new_f)
        await await_mount
        new_f.run_worker(new_f.add_widgets())

if __name__ == "__main__":
    app = TestApp()
    app.run()

exception:

Details
```
$ textual run  --dev mount_bug_reproduce.py
╭────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────╮
│ /home/comzyh/.virtualenvs/textual/lib/python3.11/site-packages/textual/widget.py:3735 in _compose                                      │
│                                                                                                                                        │
│   3732 │   │   │   self.app._handle_exception(error)                                                                                   │
│   3733 │   │   else:                                                                                                                   │
│   3734 │   │   │   self._extend_compose(widgets)                                                                                       │
│ ❱ 3735 │   │   │   await self.mount_composed_widgets(widgets)                                                                          │
│   3736 │                                                                                                                               │
│   3737 │   async def mount_composed_widgets(self, widgets: list[Widget]) -> None:                                                      │
│   3738 │   │   """Called by Textual to mount widgets after compose.                                                                    │
│                                                                                                                                        │
│ ╭────────────────────────────────────── locals ──────────────────────────────────────╮                                                 │
│ │    self = Vertical()                                                               │                                                 │
│ │ widgets = [Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label()] │                                                 │
│ ╰────────────────────────────────────────────────────────────────────────────────────╯                                                 │
│                                                                                                                                        │
│ /home/comzyh/.virtualenvs/textual/lib/python3.11/site-packages/textual/widget.py:3748 in mount_composed_widgets                        │
│                                                                                                                                        │
│   3745 │   │   │   widgets: A list of child widgets.                                                                                   │
│   3746 │   │   """                                                                                                                     │
│   3747 │   │   if widgets:                                                                                                             │
│ ❱ 3748 │   │   │   await self.mount_all(widgets)                                                                                       │
│   3749 │                                                                                                                               │
│   3750 │   def _extend_compose(self, widgets: list[Widget]) -> None:                                                                   │
│   3751 │   │   """Hook to extend composed widgets.                                                                                     │
│                                                                                                                                        │
│ ╭────────────────────────────────────── locals ──────────────────────────────────────╮                                                 │
│ │    self = Vertical()                                                               │                                                 │
│ │ widgets = [Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label()] │                                                 │
│ ╰────────────────────────────────────────────────────────────────────────────────────╯                                                 │
│                                                                                                                                        │
│ /home/comzyh/.virtualenvs/textual/lib/python3.11/site-packages/textual/widget.py:1016 in mount_all                                     │
│                                                                                                                                        │
│   1013 │   │   """                                                                                                                     │
│   1014 │   │   if self.app._exit:                                                                                                      │
│   1015 │   │   │   return AwaitMount(self, [])                                                                                         │
│ ❱ 1016 │   │   await_mount = self.mount(*widgets, before=before, after=after)                                                          │
│   1017 │   │   return await_mount                                                                                                      │
│   1018 │                                                                                                                               │
│   1019 │   if TYPE_CHECKING:                                                                                                           │
│                                                                                                                                        │
│ ╭────────────────────────────────────── locals ──────────────────────────────────────╮                                                 │
│ │   after = None                                                                     │                                                 │
│ │  before = None                                                                     │                                                 │
│ │    self = Vertical()                                                               │                                                 │
│ │ widgets = [Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label()] │                                                 │
│ ╰────────────────────────────────────────────────────────────────────────────────────╯                                                 │
│                                                                                                                                        │
│ /home/comzyh/.virtualenvs/textual/lib/python3.11/site-packages/textual/widget.py:947 in mount                                          │
│                                                                                                                                        │
│    944 │   │   if self._closing:                                                                                                       │
│    945 │   │   │   return AwaitMount(self, [])                                                                                         │
│    946 │   │   if not self.is_attached:                                                                                                │
│ ❱  947 │   │   │   raise MountError(f"Can't mount widget(s) before {self!r} is mounted")                                               │
│    948 │   │   # Check for duplicate IDs in the incoming widgets                                                                       │
│    949 │   │   ids_to_mount = [                                                                                                        │
│    950 │   │   │   widget_id for widget in widgets if (widget_id := widget.id) is not None                                             │
│                                                                                                                                        │
│ ╭────────────────────────────────────── locals ──────────────────────────────────────╮                                                 │
│ │   after = None                                                                     │                                                 │
│ │  before = None                                                                     │                                                 │
│ │    self = Vertical()                                                               │                                                 │
│ │ widgets = (Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label()) │                                                 │
│ ╰────────────────────────────────────────────────────────────────────────────────────╯                                                 │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
MountError: Can't mount widget(s) before Vertical() is mounted

In my environment, it only reproduces if I use reactive recompose.

If I remove the reactive add and uncomment 2 lines in add_widgets,

    async def add_widgets(self) -> None:
        for i in range(32):
            v = self.query_one(Vertical)
            v.mount(Label(f"Test line {i}"))
            await asyncio.sleep(0.01)

it won't happen.

Textual diagnose

$ textual diagnose

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.71.0 |
| Rich    | 13.6.0 |

## Python

| Name           | Value                                            |
|----------------|--------------------------------------------------|
| Version        | 3.11.0rc1                                        |
| Implementation | CPython                                          |
| Compiler       | GCC 11.2.0                                       |
| Executable     | /home/comzyh/.virtualenvs/textual/bin/python3.11 |

## Operating System

| Name    | Value                              |
|---------|------------------------------------|
| System  | Linux                              |
| Release | 5.15.133.1-microsoft-standard-WSL2 |
| Version | #1 SMP Thu Oct 5 21:02:42 UTC 2023 |

## Terminal

| Name                 | Value            |
|----------------------|------------------|
| Terminal Application | Windows Terminal |
| TERM                 | xterm-256color   |
| COLORTERM            | *Not set*        |
| FORCE_COLOR          | *Not set*        |
| NO_COLOR             | *Not set*        |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=138, height=78 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 138                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 78                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |

github-actions[bot] commented 2 days ago

We found the following entries 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