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

Crash opening and closing dialog really fast #4668

Closed arcivanov closed 1 week ago

arcivanov commented 1 week ago

I've been stress-testing my app and I think I found a concurrency issue in the library:

<╭────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py:3734 in _compose                                                                                                                         │
│                                                                                                                                                                                                                                       │
│   3731 │   │   │   self.app._handle_exception(error)                                            ╭────────────────────────────────────────── locals ───────────────────────────────────────────╮                                       │
│   3732 │   │   else:                                                                            │    self = TradeDialog()                                                                     │                                       │
│   3733 │   │   │   self._extend_compose(widgets)                                                │ widgets = [ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip'), TradeWidget()] │                                       │
│ ❱ 3734 │   │   │   await self.mount_composed_widgets(widgets)                                   ╰─────────────────────────────────────────────────────────────────────────────────────────────╯                                       │
│   3735 │                                                                                                                                                                                                                              │
│   3736 │   async def mount_composed_widgets(self, widgets: list[Widget]) -> None:                                                                                                                                                     │
│   3737 │   │   """Called by Textual to mount widgets after compose.                                                                                                                                                                   │
│                                                                                                                                                                                                                                       │
│ /home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py:3747 in mount_composed_widgets                                                                                                           │
│                                                                                                                                                                                                                                       │
│   3744 │   │   │   widgets: A list of child widgets.                                            ╭────────────────────────────────────────── locals ───────────────────────────────────────────╮                                       │
│   3745 │   │   """                                                                              │    self = TradeDialog()                                                                     │                                       │
│   3746 │   │   if widgets:                                                                      │ widgets = [ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip'), TradeWidget()] │                                       │
│ ❱ 3747 │   │   │   await self.mount_all(widgets)                                                ╰─────────────────────────────────────────────────────────────────────────────────────────────╯                                       │
│   3748 │                                                                                                                                                                                                                              │
│   3749 │   def _extend_compose(self, widgets: list[Widget]) -> None:                                                                                                                                                                  │
│   3750 │   │   """Hook to extend composed widgets.                                                                                                                                                                                    │
│                                                                                                                                                                                                                                       │
│ /home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py:1016 in mount_all                                                                                                                        │
│                                                                                                                                                                                                                                       │
│   1013 │   │   """                                                                              ╭────────────────────────────────────────── locals ───────────────────────────────────────────╮                                       │
│   1014 │   │   if self.app._exit:                                                               │   after = None                                                                              │                                       │
│   1015 │   │   │   return AwaitMount(self, [])                                                  │  before = None                                                                              │                                       │
│ ❱ 1016 │   │   await_mount = self.mount(*widgets, before=before, after=after)                   │    self = TradeDialog()                                                                     │                                       │
│   1017 │   │   return await_mount                                                               │ widgets = [ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip'), TradeWidget()] │                                       │
│   1018 │                                                                                        ╰─────────────────────────────────────────────────────────────────────────────────────────────╯                                       │
│   1019 │   if TYPE_CHECKING:                                                                                                                                                                                                          │
│                                                                                                                                                                                                                                       │
│ /home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py:947 in mount                                                                                                                             │
│                                                                                                                                                                                                                                       │
│    944 │   │   if self._closing:                                                                ╭────────────────────────────────────────── locals ───────────────────────────────────────────╮                                       │
│    945 │   │   │   return AwaitMount(self, [])                                                  │   after = None                                                                              │                                       │
│    946 │   │   if not self.is_attached:                                                         │  before = None                                                                              │                                       │
│ ❱  947 │   │   │   raise MountError(f"Can't mount widget(s) before {self!r} is mounted")        │    self = TradeDialog()                                                                     │                                       │
│    948 │   │   # Check for duplicate IDs in the incoming widgets                                │ widgets = (ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip'), TradeWidget()) │                                       │
│    949 │   │   ids_to_mount = [                                                                 ╰─────────────────────────────────────────────────────────────────────────────────────────────╯                                       │
│    950 │   │   │   widget_id for widget in widgets if (widget_id := widget.id) is not None                                                                                                                                            │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
MountError: Can't mount widget(s) before TradeDialog() is mounted
github-actions[bot] commented 1 week ago

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

arcivanov commented 1 week ago

Another crash looking like it's related (same concurrency issue):

Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 542, in _pre_process
    await self._dispatch_message(events.Mount())
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 650, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 719, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 740, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 45, in _invoke
    result = callback(*params[:parameter_count])
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widgets/_select.py", line 491, in _on_mount
    self._setup_options_renderables()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widgets/_select.py", line 413, in _setup_options_renderables
    option_list = self.query_one(SelectOverlay)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/dom.py", line 1340, in query_one
    return query.only_one() if expect_type is None else query.only_one(expect_type)
           ^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/css/query.py", line 278, in only_one
    self.first(expect_type) if expect_type is not None else self.first()
                                                            ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/css/query.py", line 247, in first
    raise NoMatches(f"No nodes match {self!r} on {self.node!r}")
textual.css.query.NoMatches: No nodes match <DOMQuery query='SelectOverlay'> on Select(id='class_value')
TomJGooding commented 1 week ago

Could you provide an MRE or steps to reproduce?

I've just tried rapidly opening and closing the Select using the simple app below and can't reproduce the crash on the latest Textual v0.70.0.

from textual.app import App, ComposeResult
from textual.widgets import Select

LINES = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.""".splitlines()

class SelectApp(App):

    def compose(self) -> ComposeResult:
        yield Select((line, line) for line in LINES)

if __name__ == "__main__":
    app = SelectApp()
    app.run()
arcivanov commented 1 week ago

It's a tough one, unfortunately. It's a proprietary app with a complex dialog with Select and Input components populated by network calls via workers.

But the structure is as follows and 'o' followed by 'escape' cycling really really fast produced the above two exceptions. Meanwhile TradeDialog was dynamically filling in the various fields via async workers while being dismissed, is my guess.


class TradeDialog(ModalScreen):
    BINDINGS = [("escape", "dismiss", "Cancel")]  
    ...

class DialogApp(App):
    BINDINGS = [("o", "new_order", "New Order")]

    ...

    @work(exclusive=True, group="DialogApp.action_new_order")
    async def action_new_order(self) -> None:
        await self.push_screen_wait(TradeDialog())
willmcgugan commented 1 week ago

I will need an MRE for this. It could very well be a concurrency issue, but without a place to start I couldn't say what the problem is.

When you say stress testing, are you hammering keys or doing anything automated?

willmcgugan commented 1 week ago

Can you reproduce it with this? Or modify this code until it does reproduce the issue?

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

class StressScreen(ModalScreen):
    BINDINGS = [Binding("escape", "dismiss", "dismiss", priority=True)]

    def compose(self) -> ComposeResult:
        yield Label("foo")
        yield Select([("Hello", "hello"), ("World", "world")])
        yield Select([("Foo", "foo"), ("bar", "bar")])
        yield Footer()

class StressApp(App):
    BINDINGS = [Binding("space", "modal", "modal")]

    def compose(self) -> ComposeResult:
        yield Label("BAR")
        # yield Select([("Foo", "foo"), ("bar", "bar")])
        yield Footer()

    @work(exclusive=True)
    async def action_modal(self) -> None:
        await self.push_screen(StressScreen())

if __name__ == "__main__":
    app = StressApp()
    app.run()
arcivanov commented 1 week ago

I will need an MRE for this. It could very well be a concurrency issue, but without a place to start I couldn't say what the problem is.

When you say stress testing, are you hammering keys or doing anything automated?

I understand that you need an MRE and I'm trying to see if I can provide that for you.

Yes, it was just finger testing, I bumped into the problem ensuring responsive population of the dialog which resulted in me bumping into that error.

arcivanov commented 1 week ago

Two more separate failures doing the same thing. These seem all to be focused on there not being a SelectOverlay.

One (also causing a shutdown deadlock):

06-23 11:43:57.316 ERROR    bt.ui [__init__:593]: Unhandled exception occurred
Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 542, in _pre_process
    await self._dispatch_message(events.Mount())
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 650, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 719, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 740, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 45, in _invoke
    result = callback(*params[:parameter_count])
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widgets/_select.py", line 491, in _on_mount
    self._setup_options_renderables()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widgets/_select.py", line 413, in _setup_options_renderables
    option_list = self.query_one(SelectOverlay)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/dom.py", line 1340, in query_one
    return query.only_one() if expect_type is None else query.only_one(expect_type)
           ^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/css/query.py", line 278, in only_one
    self.first(expect_type) if expect_type is not None else self.first()
                                                            ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/css/query.py", line 247, in first
    raise NoMatches(f"No nodes match {self!r} on {self.node!r}")
textual.css.query.NoMatches: No nodes match <DOMQuery query='SelectOverlay'> on Select(id='class_value')
06-23 11:44:02.769 CRITICAL bt.trader [trade:732]: Critical failures occurred - shutting down
  | ExceptionGroup: Critical failures occurred - shutting down (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/home/arcivanov/Documents/src/karellen/app/src/main/python/app/trade.py", line 700, in _ui_loop
    |     await trader_app.run_async()
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 1572, in run_async
    |     await app._shutdown()
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 2804, in _shutdown
    |     await self._close_all()
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 2784, in _close_all
    |     await self._prune_node(stack_screen)
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 3445, in _prune_node
    |     raise asyncio.TimeoutError(
    | TimeoutError: Timeout waiting for [Header(), TradingClockBar(), Grid(id='content-grid'), Footer(), ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip')] to close; possible deadlock (consider changing App.CLOSE_TIMEOUT)
    | 
    +------------------------------------

Two:

06-23 12:12:10.164 ERROR    bt.ui [__init__:593]: Unhandled exception occurred
Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 542, in _pre_process
    await self._dispatch_message(events.Mount())
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 650, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 719, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 740, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 45, in _invoke
    result = callback(*params[:parameter_count])
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widgets/_select.py", line 491, in _on_mount
    self._setup_options_renderables()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widgets/_select.py", line 413, in _setup_options_renderables
    option_list = self.query_one(SelectOverlay)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/dom.py", line 1340, in query_one
    return query.only_one() if expect_type is None else query.only_one(expect_type)
           ^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/css/query.py", line 278, in only_one
    self.first(expect_type) if expect_type is not None else self.first()
                                                            ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/css/query.py", line 247, in first
    raise NoMatches(f"No nodes match {self!r} on {self.node!r}")
arcivanov commented 1 week ago

I've added a debug log (non-reordering) to the MessagePump._dispatch_message as follows:

        with self.prevent(*message._prevent):
            logger.debug(f"{self=!r}, {message=!r}, {message.time=!r}, {message._sender=!r}")

I've grepped the resulting logs as follows. The curious extract is here. I marked the Select.compose and Select.mount with **** between which you can see SelectOverlay being unmounted by the message from the App, marked with ^^^^.

The @nnnnnn are object IDs to help tracking individual objects.

06-23 16:43:28.494 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.646179957, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.494 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.494 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.496 DEBUG    textual.mp [message_pump:651]: self=BoundDataTable(id='positions')@139731281503744, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.496 DEBUG    textual.mp [message_pump:651]: self=BoundDataTable(id='positions')@139731281503744, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.496 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=ScreenSuspend(), message.time=20890.791410347, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.497 DEBUG    textual.mp [message_pump:651]: self=PositionsTabPane(id='tab-positions')@139731292308976, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.497 DEBUG    textual.mp [message_pump:651]: self=PositionsTabPane(id='tab-positions')@139731292308976, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.500 DEBUG    textual.mp [message_pump:651]: self=ContentSwitcher()@139731283045232, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.501 DEBUG    textual.mp [message_pump:651]: self=ContentSwitcher()@139731283045232, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.504 DEBUG    textual.mp [message_pump:651]: self=TabbedContent(id='tabbed-content')@139731281500480, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.504 DEBUG    textual.mp [message_pump:651]: self=TabbedContent(id='tabbed-content')@139731281500480, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.505 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731258674736, message=Compose(), message.time=20890.801108438, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.512 DEBUG    textual.mp [message_pump:651]: self=Grid(id='content-grid')@139731281499616, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.512 DEBUG    textual.mp [message_pump:651]: self=Grid(id='content-grid')@139731281499616, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.513 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730923322640, message=Compose(), message.time=20890.809339622, message._sender=SelectOverlay()@139730923322640
06-23 16:43:28.513 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730923322640, message=Mount(), message.time=20890.809402109, message._sender=SelectOverlay()@139730923322640
06-23 16:43:28.514 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929245296, message=Compose(), message.time=20890.810261499, message._sender=SelectOverlay()@139730929245296
06-23 16:43:28.514 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929245296, message=Mount(), message.time=20890.810321291, message._sender=SelectOverlay()@139730929245296
06-23 16:43:28.515 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929253216, message=Compose(), message.time=20890.811152688, message._sender=SelectOverlay()@139730929253216
06-23 16:43:28.515 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929253216, message=Mount(), message.time=20890.811217529, message._sender=SelectOverlay()@139730929253216
06-23 16:43:28.516 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730928933568, message=Compose(), message.time=20890.812130669, message._sender=SelectOverlay()@139730928933568
06-23 16:43:28.516 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730928933568, message=Mount(), message.time=20890.812194138, message._sender=SelectOverlay()@139730928933568
06-23 16:43:28.516 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.516 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.527 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731258674736, message=Mount(), message.time=20890.823177727, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.530 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731258674736, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.825941763, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.539 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730923322640, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f15a40c76b0>, option_id=None, option_index=1), message.time=20890.825129853, message._sender=SelectOverlay()@139730923322640
06-23 16:43:28.539 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929245296, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f15a466e150>, option_id=None, option_index=2), message.time=20890.827962497, message._sender=SelectOverlay()@139730929245296
06-23 16:43:28.539 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929253216, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f15a4411160>, option_id=None, option_index=1), message.time=20890.831229074, message._sender=SelectOverlay()@139730929253216
06-23 16:43:28.539 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730928933568, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f15b1bc3bc0>, option_id=None, option_index=1), message.time=20890.833990736, message._sender=SelectOverlay()@139730928933568
06-23 16:43:28.541 DEBUG    textual.mp [message_pump:651]: self=Grid(id='data-grid')@139731258975120, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.825941763, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.542 DEBUG    textual.mp [message_pump:651]: self=TradeWidget()@139731262108400, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.825941763, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.548 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=ScreenResume(), message.time=20890.791896767, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.576 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.809516684, message._sender=SelectOverlay()@139730923322640
06-23 16:43:28.576 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.810428922, message._sender=SelectOverlay()@139730929245296
06-23 16:43:28.576 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.811339157, message._sender=SelectOverlay()@139730929253216
06-23 16:43:28.576 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.812300027, message._sender=SelectOverlay()@139730928933568
06-23 16:43:28.577 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(Select(id='class_value')), message.time=20890.826033725, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.577 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Layout(), message.time=20890.826049194, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.577 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.834999135, message._sender=SelectOverlay()@139730923322640
06-23 16:43:28.577 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.835157371, message._sender=SelectOverlay()@139730929245296
06-23 16:43:28.577 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.835281594, message._sender=SelectOverlay()@139730929253216
06-23 16:43:28.577 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(SelectOverlay()), message.time=20890.835398703, message._sender=SelectOverlay()@139730928933568
06-23 16:43:28.583 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.825941763, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.616 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731258674736, message=Resize(size=Size(width=38, height=3), virtual_size=Size(width=38, height=3)), message.time=20890.849074263, message._sender=TradeDialog()@139730932036144
06-23 16:43:28.616 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731258674736, message=Show(), message.time=20890.849266723, message._sender=TradeDialog()@139730932036144
06-23 16:43:28.639 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Update(Select(id='class_value')), message.time=20890.912467759, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.639 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=Layout(), message.time=20890.912470644, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.640 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.754974334, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.645 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730932036144, message=ScreenSuspend(), message.time=20890.936814161, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.646 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=ScreenResume(), message.time=20890.936869906, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.649 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730923322640, message=Unmount(), message.time=20890.945755346, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.650 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929245296, message=Unmount(), message.time=20890.946170864, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.650 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929253216, message=Unmount(), message.time=20890.946530547, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.651 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730928933568, message=Unmount(), message.time=20890.946916921, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.651 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731258674736, message=Unmount(), message.time=20890.94712995, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.653 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.778885442, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.654 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=ScreenSuspend(), message.time=20890.949806422, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.664 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139730787770656, message=Compose(), message.time=20890.960245431, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.675 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786701520, message=Compose(), message.time=20890.970911665, message._sender=SelectOverlay()@139730786701520
06-23 16:43:28.675 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786701520, message=Mount(), message.time=20890.971021641, message._sender=SelectOverlay()@139730786701520
06-23 16:43:28.676 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786709488, message=Compose(), message.time=20890.972492285, message._sender=SelectOverlay()@139730786709488
06-23 16:43:28.676 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786709488, message=Mount(), message.time=20890.972594286, message._sender=SelectOverlay()@139730786709488
06-23 16:43:28.678 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929243808, message=Compose(), message.time=20890.974516606, message._sender=SelectOverlay()@139730929243808
06-23 16:43:28.678 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929243808, message=Mount(), message.time=20890.974591446, message._sender=SelectOverlay()@139730929243808
06-23 16:43:28.680 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786807456, message=Compose(), message.time=20890.975994714, message._sender=SelectOverlay()@139730786807456
06-23 16:43:28.680 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786807456, message=Mount(), message.time=20890.976070196, message._sender=SelectOverlay()@139730786807456
06-23 16:43:28.691 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139730787770656, message=Mount(), message.time=20890.987516952, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.694 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139730787770656, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.990670588, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.705 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786701520, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f159be7cce0>, option_id=None, option_index=1), message.time=20890.989824002, message._sender=SelectOverlay()@139730786701520
06-23 16:43:28.705 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786709488, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f15b1c9a150>, option_id=None, option_index=2), message.time=20890.992779337, message._sender=SelectOverlay()@139730786709488
06-23 16:43:28.705 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929243808, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f159be94980>, option_id=None, option_index=1), message.time=20890.995767322, message._sender=SelectOverlay()@139730929243808
06-23 16:43:28.705 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786807456, message=OptionHighlighted(option_list=SelectOverlay(), option=<textual.widgets._option_list.Option object at 0x7f159be96900>, option_id=None, option_index=1), message.time=20891.000238856, message._sender=SelectOverlay()@139730786807456
06-23 16:43:28.708 DEBUG    textual.mp [message_pump:651]: self=Grid(id='data-grid')@139730786490976, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.990670588, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.709 DEBUG    textual.mp [message_pump:651]: self=TradeWidget()@139730787755808, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.990670588, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.715 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=ScreenResume(), message.time=20890.95016878, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.743 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20890.971202931, message._sender=SelectOverlay()@139730786701520
06-23 16:43:28.743 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20890.972787468, message._sender=SelectOverlay()@139730786709488
06-23 16:43:28.743 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20890.974790068, message._sender=SelectOverlay()@139730929243808
06-23 16:43:28.743 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20890.976201772, message._sender=SelectOverlay()@139730786807456
06-23 16:43:28.744 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(Select(id='class_value')), message.time=20890.990762139, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.744 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Layout(), message.time=20890.990767629, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.744 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20891.001371867, message._sender=SelectOverlay()@139730786701520
06-23 16:43:28.744 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20891.001539521, message._sender=SelectOverlay()@139730786709488
06-23 16:43:28.744 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20891.001671679, message._sender=SelectOverlay()@139730929243808
06-23 16:43:28.744 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(SelectOverlay()), message.time=20891.001790461, message._sender=SelectOverlay()@139730786807456
06-23 16:43:28.752 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.990670588, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.784 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139730787770656, message=Resize(size=Size(width=38, height=3), virtual_size=Size(width=38, height=3)), message.time=20891.015806028, message._sender=TradeDialog()@139730923321152
06-23 16:43:28.784 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139730787770656, message=Show(), message.time=20891.015939638, message._sender=TradeDialog()@139730923321152
06-23 16:43:28.805 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Update(Select(id='class_value')), message.time=20891.08043632, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.805 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Layout(), message.time=20891.080439095, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.806 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.825941763, message._sender=Select(id='class_value')@139731258674736
06-23 16:43:28.807 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.921027278, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.807 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.926305472, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.808 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Changed(Select(id='class_value'), 'equity'), message.time=20890.990670588, message._sender=Select(id='class_value')@139730787770656
06-23 16:43:28.808 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20891.047642878, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.830 DEBUG    textual.mp [message_pump:651]: self=TradeWidget()@139730787755808, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.921027278, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.831 DEBUG    textual.mp [message_pump:651]: self=TradeWidget()@139730787755808, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.926305472, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.831 DEBUG    textual.mp [message_pump:651]: self=TradeWidget()@139730787755808, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20891.047642878, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.831 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20891.09018162, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.831 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.921027278, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.831 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.926305472, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.832 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20891.047642878, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.832 DEBUG    textual.mp [message_pump:651]: self=TradeWidget()@139730787755808, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20891.09018162, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.832 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20891.09018162, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.834 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20890.921027278, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.834 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20890.926305472, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.838 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=ScreenResume(), message.time=20891.130593378, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.843 DEBUG    textual.mp [message_pump:651]: self=TradeDialog()@139730923321152, message=ScreenSuspend(), message.time=20891.130505994, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.845 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786701520, message=Unmount(), message.time=20891.141029221, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.845 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786709488, message=Unmount(), message.time=20891.141415444, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.845 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730929243808, message=Unmount(), message.time=20891.141799213, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.846 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730786807456, message=Unmount(), message.time=20891.142143948, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.846 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139730787770656, message=Unmount(), message.time=20891.142328985, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.849 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='o', character='o', name='o', is_printable=True), message.time=20891.047642878, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.850 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=ScreenSuspend(), message.time=20891.145974843, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
**** 06-23 16:43:28.859 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731151420544, message=Compose(), message.time=20891.155474172, message._sender=Select(id='class_value')@139731151420544
06-23 16:43:28.867 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783297632, message=Compose(), message.time=20891.16355812, message._sender=SelectOverlay()@139730783297632
06-23 16:43:28.867 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783297632, message=Mount(), message.time=20891.163623453, message._sender=SelectOverlay()@139730783297632
06-23 16:43:28.869 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783305648, message=Compose(), message.time=20891.165732833, message._sender=SelectOverlay()@139730783305648
06-23 16:43:28.869 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783305648, message=Mount(), message.time=20891.165807383, message._sender=SelectOverlay()@139730783305648
06-23 16:43:28.870 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783756096, message=Compose(), message.time=20891.166716325, message._sender=SelectOverlay()@139730783756096
06-23 16:43:28.870 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783756096, message=Mount(), message.time=20891.166784813, message._sender=SelectOverlay()@139730783756096
06-23 16:43:28.871 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783764064, message=Compose(), message.time=20891.167669299, message._sender=SelectOverlay()@139730783764064
06-23 16:43:28.871 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783764064, message=Mount(), message.time=20891.167736375, message._sender=SelectOverlay()@139730783764064
06-23 16:43:28.873 DEBUG    textual.mp [message_pump:651]: self=TraderApp(title='App', classes={'-dark-mode'})@139731289724768, message=Key(key='escape', character='\x1b', name='escape', is_printable=False, aliases=['escape', 'ctrl+left_square_brace']), message.time=20891.09018162, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
06-23 16:43:28.878 DEBUG    textual.mp [message_pump:651]: self=Screen(id='_default')@139731283636352, message=ScreenResume(), message.time=20891.169215194, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
^^^^ 06-23 16:43:28.892 DEBUG    textual.mp [message_pump:651]: self=SelectOverlay()@139730783297632, message=Unmount(), message.time=20891.18787608, message._sender=TraderApp(title='App', classes={'-dark-mode'})@139731289724768
**** 06-23 16:43:28.892 DEBUG    textual.mp [message_pump:651]: self=Select(id='class_value')@139731151420544, message=Mount(), message.time=20891.188456246, message._sender=Select(id='class_value')@139731151420544

!!!!!!!!!!
06-23 16:43:28.892 ERROR    bt.ui [__init__:594]: Unhandled exception occurred
textual.css.query.NoMatches: No nodes match <DOMQuery query='SelectOverlay'> on Select(id='class_value')
!!!!!!!!!!

selectoverlay_unmount.log

TomJGooding commented 1 week ago

Please stop just spamming tracebacks and logs from your proprietary app.

You say that you understand that this needs an MRE, but I'll try explaining this more bluntly. Without any way to reproduce the issue you're describing, this is impossible to debug. You can't expect anyone to understand exactly what your code is doing only based on the tracebacks or logs.

I had no idea this even involved a ModalScreen from your original issue post.

arcivanov commented 1 week ago

I have the MRE. The culprit is the number of controls being composed. With just 2 controls the crash is not reproducible. As the number of composed controls grows (i.e. range(20)) the problems becomes increasingly easy to reproduce. The core failure is unfortunately obscured by a shutdown deadlock so a logging system has to be used to capture the failure (I overwrote the _handle_exception to log it).

import logging
from typing import Type

from textual import work
from textual._path import CSSPathType
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.driver import Driver
from textual.screen import ModalScreen
from textual.widgets import Label, Select, Footer

logger = logging.getLogger("bt.stress")

class StressScreen(ModalScreen):
    BINDINGS = [Binding("escape", "dismiss", "dismiss")]

    def __init__(self, data, name: str | None = None, id: str | None = None, classes: str | None = None) -> None:
        super().__init__(name, id, classes)
        self.data = dict(data)

    def compose(self) -> ComposeResult:
        yield Select([("Mode A", "A"), ("Mode B", "B")], id="mode_value",
                     value=self.data.get("mode", Select.BLANK))
        if self.data.get("mode") == "B":
            yield Select([("Foo", "foo"), ("bar", "bar")], id="dependent_value",
                         value=self.data.get("dependent", Select.BLANK))

        for idx in range(20):
            yield Select([(f"Foo{idx}", f"foo{idx}"), (f"Bar{idx}", f"bar{idx}")],
                         id=f"independent{idx}_value",
                         value=self.data.get(f"independendent{idx}", Select.BLANK))

    async def on_select_changed(self, message: Select.Changed) -> None:
        option_name = message.select.id[:-6]
        if message.select.id == "mode_value":
            if message.value != self.data.get(option_name):
                self.data[option_name] = message.value
                self._recompose()
                return

        self.data[option_name] = message.value

    @work(exclusive=True, group="StressScreen.recompose")
    async def _recompose(self):
        await self.recompose()

class StressApp(App):
    BINDINGS = [Binding("o", "modal", "modal")]

    def __init__(self, driver_class: Type[Driver] | None = None, css_path: CSSPathType | None = None,
                 watch_css: bool = False):
        super().__init__(driver_class, css_path, watch_css)
        self.data = {"mode": "B", "dependent": "bar"}

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

    @work(exclusive=True)
    async def action_modal(self) -> None:
        await self.push_screen_wait(StressScreen(self.data))

    def _handle_exception(self, error: Exception) -> None:
        logger.exception("Unhandled exception occurred", exc_info=error)
        super()._handle_exception(error)

Three to five rapid pairs of o-ESC produce the below:

06-23 21:33:00.720 ERROR    bt.stress [stress:67]: Unhandled exception occurred
Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 540, in _pre_process
    await self._dispatch_message(events.Compose())
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 655, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 724, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 745, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 47, in _invoke
    result = await result
             ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 3720, in _on_compose
    await self._compose()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 3734, in _compose
    await self.mount_composed_widgets(widgets)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 3747, in mount_composed_widgets
    await self.mount_all(widgets)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 1016, in mount_all
    await_mount = self.mount(*widgets, before=before, after=after)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 947, in mount
    raise MountError(f"Can't mount widget(s) before {self!r} is mounted")
textual.widget.MountError: Can't mount widget(s) before StressScreen() is mounted

followed by

06-23 21:33:06.368 CRITICAL bt.trader [trade:734]: Critical failures occurred - shutting down
  | ExceptionGroup: Critical failures occurred - shutting down (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/home/arcivanov/Documents/src/karellen/app/src/main/python/app/trade.py", line 702, in _ui_loop
    |     await stress_app.run_async()
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 1572, in run_async
    |     await app._shutdown()
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 2804, in _shutdown
    |     await self._close_all()
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 2784, in _close_all
    |     await self._prune_node(stack_screen)
    |   File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 3445, in _prune_node
    |     raise asyncio.TimeoutError(
    | TimeoutError: Timeout waiting for [Label(), Footer(), ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip')] to close; possible deadlock (consider changing App.CLOSE_TIMEOUT)
    | 
    +------------------------------------

Log is attached. stress-log-2024-06-23T21:32:53.825.log

arcivanov commented 1 week ago

The MRE can be even further simplified. Recomposition and dynamic structure is irrelevant. The issue pops up the more eagerly the more controls are on the modal screen. The last crash occurred with just two o-ESC sequences.

import sys

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

exc: Exception | None = None

class StressScreen(ModalScreen):
    BINDINGS = [Binding("escape", "dismiss", "dismiss")]

    def compose(self) -> ComposeResult:
        for idx in range(40):
            yield Select([(f"Foo{idx}", f"foo{idx}"), (f"Bar{idx}", f"bar{idx}")],
                         id=f"independent{idx}_value",
                         value=Select.BLANK)

class StressApp(App):
    BINDINGS = [Binding("o", "modal", "modal")]

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

    @work(exclusive=True)
    async def action_modal(self) -> None:
        await self.push_screen_wait(StressScreen())

    def _handle_exception(self, error: Exception) -> None:
        global exc
        exc = error
        super()._handle_exception(error)

if __name__ == "__main__":
    try:
        StressApp().run()
    finally:
        if exc:
            sys.excepthook(type(exc), exc, None)
Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 540, in _pre_process
    await self._dispatch_message(events.Compose())
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 655, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 724, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/message_pump.py", line 745, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/_callback.py", line 47, in _invoke
    result = await result
             ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 3720, in _on_compose
    await self._compose()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 3734, in _compose
    await self.mount_composed_widgets(widgets)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 3747, in mount_composed_widgets
    await self.mount_all(widgets)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 1016, in mount_all
    await_mount = self.mount(*widgets, before=before, after=after)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/widget.py", line 947, in mount
    raise MountError(f"Can't mount widget(s) before {self!r} is mounted")
textual.widget.MountError: Can't mount widget(s) before StressScreen() is mounted
Traceback (most recent call last):
  File "/home/arcivanov/Documents/src/karellen/app/src/main/python/app/stress.py", line 44, in <module>
    StressApp().run()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 1620, in run
    asyncio.run(run_app())
  File "/home/arcivanov/.pyenv/versions/3.12.3/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 1606, in run_app
    await self.run_async(
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 1572, in run_async
    await app._shutdown()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 2804, in _shutdown
    await self._close_all()
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 2784, in _close_all
    await self._prune_node(stack_screen)
  File "/home/arcivanov/.pyenv/versions/app/lib/python3.12/site-packages/textual/app.py", line 3445, in _prune_node
    raise asyncio.TimeoutError(
TimeoutError: Timeout waiting for [Label(), Footer(), ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip')] to close; possible deadlock (consider changing App.CLOSE_TIMEOUT)
github-actions[bot] commented 1 week ago

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

arcivanov commented 1 week ago

I've installed the main branch and the problem still persists @willmcgugan

willmcgugan commented 1 week ago

With what? Your last MRE?

I can no longer reproduce it in main.

Could you paste the output of the following command:

textual diagnose
arcivanov commented 1 week ago

So there were two crashes in this bug:

textual.widget.MountError: Can't mount widget(s) before StressScreen() is mounted

and

textual.css.query.NoMatches: No nodes match <DOMQuery query='SelectOverlay'> on Select(id='class_value')

The MRE reproduced the first one, but I never got to the second one because the MountError was blocking.

Now the second one is still there even though the MRE doesn't crash anymore.

Let me see if I can modify the MRE to reproduce the second one.

arcivanov commented 1 week ago

Actually just using MRE I just triggered a whole bunch of other issues as well:

Textual Diagnostics

Versions

Name Value
Textual 0.70.0
Rich 13.7.1

Python

Name Value
Version 3.12.3
Implementation CPython
Compiler GCC 13.2.1 20240316 (Red Hat 13.2.1-7)
Executable /home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/bin/python

Operating System

Name Value
System Linux
Release 6.9.5-200.fc40.x86_64
Version #1 SMP PREEMPT_DYNAMIC Sun Jun 16 15:47:09 UTC 2024

Terminal

Name Value
Terminal Application Unknown
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

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

Just o-ESC of the stress.py just gave me this:

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

NOTE: 1 of 80 errors shown. Run with textual run --dev to see all errors.
Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 540, in _pre_process
    await self._dispatch_message(events.Compose())
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 655, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 724, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 745, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/_callback.py", line 47, in _invoke
    result = await result
             ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 3720, in _on_compose
    await self._compose()
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 3734, in _compose
    await self.mount_composed_widgets(widgets)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 3747, in mount_composed_widgets
    await self.mount_all(widgets)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 1016, in mount_all
    await_mount = self.mount(*widgets, before=before, after=after)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 947, in mount
    raise MountError(f"Can't mount widget(s) before {self!r} is mounted")
textual.widget.MountError: Can't mount widget(s) before Select(id='independent39_value') is mounted
arcivanov commented 1 week ago

Nope, the original problem is still there:

$ textual run --dev stress.py 
Traceback (most recent call last):
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 540, in _pre_process
    await self._dispatch_message(events.Compose())
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 655, in _dispatch_message
    await self.on_event(message)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 724, in on_event
    await self._on_message(event)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/message_pump.py", line 745, in _on_message
    await invoke(method, message)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/_callback.py", line 81, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/_callback.py", line 47, in _invoke
    result = await result
             ^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 3720, in _on_compose
    await self._compose()
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 3734, in _compose
    await self.mount_composed_widgets(widgets)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 3747, in mount_composed_widgets
    await self.mount_all(widgets)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 1016, in mount_all
    await_mount = self.mount(*widgets, before=before, after=after)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/widget.py", line 947, in mount
    raise MountError(f"Can't mount widget(s) before {self!r} is mounted")
textual.widget.MountError: Can't mount widget(s) before StressScreen() is mounted
Traceback (most recent call last):
  File "/home/arcivanov/Documents/src/karellen/boris-trading/src/main/python/stress.py", line 41, in <module>
    StressApp().run()
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/app.py", line 1620, in run
    asyncio.run(run_app())
  File "/home/arcivanov/.pyenv/versions/3.12.3/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/app.py", line 1606, in run_app
    await self.run_async(
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/app.py", line 1572, in run_async
    await app._shutdown()
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/app.py", line 2804, in _shutdown
    await self._close_all()
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/app.py", line 2784, in _close_all
    await self._prune_node(stack_screen)
  File "/home/arcivanov/.pyenv/versions/3.12.3/envs/boris-trading/lib/python3.12/site-packages/textual/app.py", line 3445, in _prune_node
    raise asyncio.TimeoutError(
TimeoutError: Timeout waiting for [Label(), Footer(), ToastRack(id='textual-toastrack'), Tooltip(id='textual-tooltip')] to close; possible deadlock (consider changing App.CLOSE_TIMEOUT)
arcivanov commented 1 week ago

I've updated using pip install -U git+https://github.com/Textualize/textual

arcivanov commented 1 week ago

Curiously, I'm unable to trigger the case via a pilot, but fairly reliably can trigger it with my fingers. Also, headed pilot seems rather slow in comparison to what I can do by hand.

import asyncio
import sys

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

exc: Exception | None = None

class StressScreen(ModalScreen):
    BINDINGS = [Binding("escape", "dismiss", "dismiss")]

    def compose(self) -> ComposeResult:
        for idx in range(40):
            yield Select([(f"Foo{idx}", f"foo{idx}"), (f"Bar{idx}", f"bar{idx}")],
                         id=f"independent{idx}_value",
                         value=Select.BLANK)

class StressApp(App):
    BINDINGS = [Binding("o", "modal", "modal")]

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

    @work(exclusive=True)
    async def action_modal(self) -> None:
        await self.push_screen_wait(StressScreen())

    def _handle_exception(self, error: Exception) -> None:
        global exc
        exc = error
        super()._handle_exception(error)

if __name__ == "__main__":

    async def test():
        app = StressApp()
        try:
            async with app.run_test(headless=False, size=None) as pilot:
                while True:
                    await pilot.press("o")
                    await pilot.press("escape")
                    await asyncio.sleep(0)
        finally:
            if exc:
                sys.excepthook(type(exc), exc, None)

    asyncio.run(test())
willmcgugan commented 1 week ago

The pilot waits for messages to be processed, unlike manual keypresses.

From your recent traceback I can tell you are not running the latest Textual. Please check you have installed Textual main in to the same venv.

arcivanov commented 1 week ago

Ok I will recheck. What's the standard way of installing the "latest" textual if not via pip install -U git+https://github.com/Textualize/textual ?

willmcgugan commented 1 week ago

That should work if you have the venv activated. You could also clone it and run pip install -e .

If pip is installed globally, you can use this to ensure it uses the same env as the python command.

python -m pip 
TomJGooding commented 1 week ago

For what it's worth, after pulling the latest changes on my fork of Textual, I can't reproduce the issue using the simplified MRE in https://github.com/Textualize/textual/issues/4668#issuecomment-2186463224 when mashing the o and Esc keys.

arcivanov commented 1 week ago

Yep, I removed the textual completely, installed it from the tree and can no longer bang out any of the problem.