Skip to content

Crash when closing notification - KeyError: "No 'toast--title' key in COMPONENT_CLASSES" #5646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mzebrak opened this issue Mar 13, 2025 · 5 comments

Comments

@mzebrak
Copy link

mzebrak commented Mar 13, 2025

I think it is related to: #5629

Best to have some stress on your machine. I've got a basic Ryzen 5600x
and
stress --cpu 4 --timeout 600s --vm 4 --io 4 --vm-bytes 1024M

is enough for me.

To reproduce, you just need to spam "LMB" on the notification to close it, it will crash. If there is no notification title, it will crash with same error as in #5629

MRE:

from __future__ import annotations

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


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        yield Static("Close notifications as fast as you can :)")

    def on_mount(self) -> None:
        self.set_interval(0.2, lambda: self.notify(title="Press me to close!", message="not me"))


if __name__ == "__main__":
    ExampleApp().run()

Stacktrace:

╭───────────────────────────────────────────────────────── Traceback (most recent call last) ─────────────────────────────────────────────────────────╮
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/app.py:3748 in on_event                                            │
│                                                                                                                                                     │
│   3745 │   │   │   │   │   │   # Shouldn't occur, since at the very least this will find the Sc                                                     │3746 │   │   │   │   │   │   self._mouse_down_widget = None                                                                                       │
│   3747 │   │   │   │                                                                                                                                │
│ ❱ 3748 │   │   │   │   self.screen._forward_event(event)                                                                                            │
│   3749 │   │   │   │                                                                                                                                │
│   3750 │   │   │   │   # If a MouseUp occurs at the same widget as a MouseDown, then we should                                                      │3751 │   │   │   │   # consider it a click, and produce a Click event.                                                                            │
│                                                                                                                                                     │
│ ╭───────────────────────────────────────────── locals ─────────────────────────────────────────────╮                                                │
│ │     _ = Region(x=89, y=35, width=60, height=4)                                                   │                                                │
│ │ event = MouseDown(None, x=103, y=36, pointer_x=103.0, pointer_y=36.0, button=1)                  │                                                │
│ │  self = ExampleApp(title='ExampleApp', classes={'-dark-mode'}, pseudo_classes={'focus', 'dark'}) │                                                │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯                                                │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/screen.py:1533 in _forward_event                                   │
│                                                                                                                                                     │
│   1530 │   │   │   elif isinstance(event, events.MouseDown) and not self.app.mouse_captured:                                                        │
│   1531 │   │   │   │   self._box_select = event.shift                                                                                               │
│   1532 │   │   │   │   self._mouse_down_offset = event.screen_offset                                                                                │
│ ❱ 1533 │   │   │   │   select_widget, select_offset = self.get_widget_and_offset_at(                                                                │
│   1534 │   │   │   │   │   event.screen_x, event.screen_y                                                                                           │
│   1535 │   │   │   │   )                                                                                                                            │
│   1536 │   │   │   │   if (                                                                                                                         │
│                                                                                                                                                     │
│ ╭──────────────────────────────────── locals ─────────────────────────────────────╮                                                                 │
│ │ event = MouseDown(None, x=103, y=36, pointer_x=103.0, pointer_y=36.0, button=1) │                                                                 │
│ │  self = Screen(id='_default')                                                   │                                                                 │
│ ╰─────────────────────────────────────────────────────────────────────────────────╯                                                                 │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/screen.py:634 in get_widget_and_offset_at                          │
│                                                                                                                                                     │
│    631 │   │   Returns:                                                                         ╭─────────── locals ───────────╮                    │
│    632 │   │   │   Tuple of Widget and Offset, both of which may be None.                       │ self = Screen(id='_default') │                    │
│    633 │   │   """                                                                              │    x = 103                   │                    │
│ ❱  634 │   │   return self._compositor.get_widget_and_offset_at(x, y)                           │    y = 36                    │                    │
│    635 │                                                                                        ╰──────────────────────────────╯                    │
│    636 │   def find_widget(self, widget: Widget) -> MapGeometry:                                                                                    │
│    637 │   │   """Get the screen region of a Widget.                                                                                                │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/_compositor.py:904 in get_widget_and_offset_at                     │
│                                                                                                                                                     │
│    901 │   │   y -= region.y + gutter_right                                                                                                         │
│    902 │   │                                                                                                                                        │
│    903 │   │   visible_screen_stack.set(widget.app._background_screens)                                                                             │
│ ❱  904 │   │   line = widget.render_line(y)                                                                                                         │
│    905 │   │                                                                                                                                        │
│    906 │   │   end = 0                                                                                                                              │
│    907 │   │   start = 0                                                                                                                            │
│                                                                                                                                                     │
│ ╭──────────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────────╮                         │
│ │  gutter_left = 2                                                                                                        │                         │
│ │ gutter_right = 1                                                                                                        │                         │
│ │       region = Region(x=89, y=35, width=60, height=4)                                                                   │                         │
│ │         self = <Compositor size=Size(width=151, height=40) widgets={Static(), Toast(), Screen(id='_default'), Toast()}> │                         │
│ │       widget = Toast()                                                                                                  │                         │
│ │            x = -90                                                                                                      │                         │
│ │            y = -36                                                                                                      │                         │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                         │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/widget.py:3902 in render_line                                      │
│                                                                                                                                                     │
│   3899 │   │   │   A rendered line.                                                             ╭──── locals ────╮                                  │
│   3900 │   │   """                                                                              │ self = Toast() │                                  │
│   3901 │   │   if self._dirty_regions:                                                          │    y = -36     │                                  │
│ ❱ 3902 │   │   │   self._render_content()                                                       ╰────────────────╯                                  │
│   3903 │   │   try:                                                                                                                                 │
│   3904 │   │   │   line = self._render_cache.lines[y]                                                                                               │
│   3905 │   │   except IndexError:                                                                                                                   │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/widget.py:3887 in _render_content                                  │
│                                                                                                                                                     │
│   3884 │   def _render_content(self) -> None:                                                   ╭───── locals ─────╮                                │
│   3885 │   │   """Render all lines."""                                                          │ height = 0       │                                │
│   3886 │   │   width, height = self.size                                                        │   self = Toast() │                                │
│ ❱ 3887 │   │   visual = self._render()                                                          │  width = 0       │                                │
│   3888 │   │   strips = Visual.to_strips(self, visual, width, height, self.visual_style)        ╰──────────────────╯                                │
│   3889 │   │   self._render_cache = _RenderCache(self.size, strips)                                                                                 │
│   3890 │   │   self._dirty_regions.clear()                                                                                                          │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/widget.py:4110 in _render                                          │
│                                                                                                                                                     │
│   4107 │   │   if cached_visual is not None:                                                    ╭───────────── locals ─────────────╮                │
│   4108 │   │   │   assert isinstance(cached_visual, Visual)                                     │     cache_key = '_render.visual' │                │
│   4109 │   │   │   return cached_visual                                                         │ cached_visual = None             │                │
│ ❱ 4110 │   │   visual = visualize(self, self.render(), markup=self._render_markup)              │          self = Toast()          │                │
│   4111 │   │   self._layout_cache[cache_key] = visual                                           ╰──────────────────────────────────╯                │
│   4112 │   │   return visual                                                                                                                        │
│   4113                                                                                                                                              │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/widgets/_toast.py:115 in render                                    │
│                                                                                                                                                     │
│   112 │   │   """                                                                                                                                   │
│   113 │   │   notification = self._notification                                                                                                     │
│   114 │   │   if notification.title:                                                                                                                │
│ ❱ 115 │   │   │   header_style = self.get_component_rich_style("toast--title")                                                                      │
│   116 │   │   │   notification_text = Text.assemble(                                                                                                │
│   117 │   │   │   │   (notification.title, header_style),                                                                                           │
│   118 │   │   │   │   "\n",                                                                                                                         │
│                                                                                                                                                     │
│ ╭────────────────────────────── locals ───────────────────────────────╮                                                                             │
│ │ notification = Notification(                                        │                                                                             │
│ │                │   message='not me',                                │                                                                             │
│ │                │   title='Press me to close!',                      │                                                                             │
│ │                │   severity='information',                          │                                                                             │
│ │                │   raised_it=1741871626.2424397,                    │                                                                             │
│ │                │   identity='62d4cbfe-cc95-4e80-946c-a15af6778914', │                                                                             │
│ │                │   time_left=4.593467473983765,                     │                                                                             │
│ │                │   has_expired=False                                │                                                                             │
│ │                )                                                    │                                                                             │
│ │         self = Toast()                                              │                                                                             │
│ ╰─────────────────────────────────────────────────────────────────────╯                                                                             │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/widget.py:1032 in get_component_rich_style                         │
│                                                                                                                                                     │
│   1029 │   │   """                                                                              ╭────────── locals ───────────╮                     │
│   1030 │   │                                                                                    │   names = ('toast--title',) │                     │
│   1031 │   │   if names not in self._rich_style_cache:                                          │ partial = False             │                     │
│ ❱ 1032 │   │   │   component_styles = self.get_component_styles(*names)                         │    self = Toast()           │                     │
│   1033 │   │   │   style = component_styles.rich_style                                          ╰─────────────────────────────╯                     │
│   1034 │   │   │   text_opacity = component_styles.text_opacity                                                                                     │
│   1035 │   │   │   if text_opacity < 1 and style.bgcolor is not None:                                                                               │
│                                                                                                                                                     │
│ /home/mzebrak/.pyenv/versions/clive-3.10.13/lib/python3.10/site-packages/textual/dom.py:570 in get_component_styles                                 │
│                                                                                                                                                     │
│    567 │   │                                                                                    ╭──────────── locals ────────────╮                  │
│    568 │   │   for name in names:                                                               │   name = 'toast--title'        │                  │
│    569 │   │   │   if name not in self._component_styles:                                       │  names = ('toast--title',)     │                  │
│ ❱  570 │   │   │   │   raise KeyError(f"No {name!r} key in COMPONENT_CLASSES")                  │   self = Toast()               │                  │
│    571 │   │   │   component_styles = self._component_styles[name]                              │ styles = RenderStyles(Toast()) │                  │
│    572 │   │   │   styles.node = component_styles.node                                          ╰────────────────────────────────╯                  │
│    573 │   │   │   styles.base.merge(component_styles.base)                                                                                         │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
KeyError: "No 'toast--title' key in COMPONENT_CLASSES"
Copy link

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

@mzebrak mzebrak changed the title Crash when closing notification - KeyError: "No 'toast--title' key in COMPONENT_CLASSES" Crash when closing notification - KeyError: "No 'toast--title' key in COMPONENT_CLASSES" Mar 13, 2025
@TomJGooding
Copy link
Contributor

Looking at the traceback, I notice that get_widget_and_offset_at has negative x and y values. I could be wrong, but the change in #5641 might prevent this crash?

@mzebrak
Copy link
Author

mzebrak commented Mar 13, 2025

Looking at the traceback, I notice that get_widget_and_offset_at has negative x and y values. I could be wrong, but the change in #5641 might prevent this crash?

Tried again with the latest main:
pip install git+ssh://[email protected]/Textualize/textual.git@d9f7ffdad56bda27048a8bb7b70627dd90cc4c8e

But I'm still able to reproduce with the mentioned MRE.

Actually I think this fixes this issue. Initially, pip didn't update textual even though it said it did, but after manually uninstalling and reinstalling it, I can't reproduce it on the latest main

@mzebrak mzebrak closed this as completed Mar 13, 2025
Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

@TomJGooding
Copy link
Contributor

TomJGooding commented Mar 13, 2025

Glad that seems to fix it. Thanks for double-checking, my hand was getting tired trying to spam click to reproduce this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants