-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Have you checked closed issues? (https://github.com/Textualize/textual/issues?q=is%3Aissue+is%3Aclosed)
Have you checked against the most recent version of Textual? (https://pypi.org/search/?q=textual)
Consider discussions!
Issues are for actionable items only.
If Textual crashes or behaves differently from the docs, then submit an issue.
Environment:
Fedora 42
Python 3.13.11
Textual 6.11.0
uv
The bug
With "@on(events.Click, «»)" using an enum str id constant for the css selector (The "#" is part of the string) works. Using an f-string that expands the full constant fails. No error, but the handler is never called. Including the "#" as a literal in the f-string followed by an expansion of the constant with it's leading "#" stripped works.
When a single id is the selector, the constant name without an f-string wrapper is the way to go. However, that does not work when multiple ids are to be matched. In that case, a selector string in the form "#«id-1». #«id-2»" is needed. A literal string like that works. An f-string like f"#{ID_1}, #{ID_2}" works. with constants that include the "#" needed to use as a selector, f"{SEL_1}, {SEL_2}" fails. No error, never runs the handler.
uv run python bug_app.py
"""A minimal app to demonstrate a bug in Textual's @on decorator."""
from __future__ import annotations
from enum import Enum
from textual import events
from textual.app import App, ComposeResult
from textual.widgets import Label, RichLog
from textual import on
class BugId(str, Enum):
"""IDs for the bug report app."""
# This pattern will fail to register the @on handler correctly
FAILING_HANDLER = "#failing-handler"
# This pattern works by reconstructing the ID string explicitly
WORKING_F_STRING_HANDLER = "#working-f-string-handler"
# This pattern works by referencing ID constant explicitly
WORKING_CONSTANT_HANDLER = "#working-constant-handler"
LOG_DISPLAY = "#log-display"
class BugApp(App[None]):
"""A minimal app to demonstrate the @on decorator bug.
Instructions:
1. Run this script.
2. Click the "WORKING CONSTANT" label.
- You should see "on_any_click received click from: #working-constant-handler"
- You WILL see "--> SUCCESS: Handler for #working-constant-handler called!"
3. Click the "FAILING F-STRING" label.
- You should see "on_any_click received click from: #failing-handler"
- You will NOT see "--> SUCCESS: Handler for #failing-handler called!"
4. Click the "WORKING F-STRING" label.
- You should see "on_any_click received click from: #working-f-string-handler"
- You WILL see "--> SUCCESS: Handler for #working-f-string-handler called!"
- You WILL see "--> SUCCESS: Handler for multiple handlers called!"
This demonstrates that @on(events.Click, f"{BugId.FAILING_HANDLER}") fails to register
the handler, while @on(events.Click, f"#{BugId.WORKING_F_STRING_HANDLER[1:]}") works,
even though both evaluate to an identical CSS selector string, and the using
WORKING_F_STRING_HANDLER constant directly without an f-string works.
The multiple handlers case shows that the "#" needs to be explicit in the f-string for
all ids, not only the first one.
"""
CSS = """
#failing-handler {
background: #880000;
}
#working-f-string-handler {
background: #008800;
}
#working-constant-handler {
background: #008800;
}
Label {
border: round white;
width: 1fr;
}
#log-display {
height: 1fr;
border: round $secondary;
}
"""
def compose(self) -> ComposeResult:
"""Compose the app."""
yield Label(
"Click me: WORKING CONSTANT (@on selector = BugId.WORKING_CONSTANT_HANDLER)",
id=BugId.WORKING_CONSTANT_HANDLER[1:],
)
yield Label(
"Click me: FAILING F-STRING (@on selector = f'{BugId.FAILING_HANDLER}')",
id=BugId.FAILING_HANDLER[1:],
)
yield Label(
"Click me: WORKING F-STRING (@on selector = f'#{BugId.WORKING_F_STRING_HANDLER[1:]}')",
id=BugId.WORKING_F_STRING_HANDLER[1:],
)
yield RichLog(id=BugId.LOG_DISPLAY[1:], wrap=True)
def on_mount(self) -> None:
self._log("App started. Click the labels to see handler calls.")
def _log(self, message: str, ) -> None:
"""Log a message to the RichLog widget."""
log_widget = self.query_one(BugId.LOG_DISPLAY, RichLog)
log_widget.write(message)
@on(events.Click)
def on_any_click(self, event: events.Click) -> None:
"""Log any click event."""
self._log(f"on_any_click received click from: #{event.control.id}")
# --- Test Cases ---
# This uses the syntax that fails to register the handler
@on(events.Click, f"{BugId.FAILING_HANDLER}")
def on_click_failing_case(self, event: events.Click) -> None:
"""This handler should be called for #failing-handler but is not."""
self._log(f"--> SUCCESS: Handler for #{event.control.id} called! (FAILING F-STRING SYNTAX)")
# This uses the syntax that works (user's workaround)
@on(events.Click, f"#{BugId.WORKING_F_STRING_HANDLER[1:]}")
def on_click_working_case_1(self, event: events.Click) -> None:
"""This handler is correctly called for #working-f-string-handler."""
self._log(f"--> SUCCESS: Handler for #{event.control.id} called! (WORKING F-STRING SYNTAX)")
# This uses the (single) constant value directly
@on(events.Click, BugId.WORKING_CONSTANT_HANDLER)
def on_click_working_case_2(self, event: events.Click) -> None:
"""This handler is correctly called for #working-constant-handler."""
self._log(f"--> SUCCESS: Handler for #{event.control.id} called! (WORKING CONSTANT SYNTAX)")
# This uses a compound selector, first works, second fails
@on(events.Click, f"#{BugId.WORKING_F_STRING_HANDLER[1:]}, {BugId.WORKING_CONSTANT_HANDLER}")
def on_click_compound_case(self, event: events.Click) -> None:
"""This handler is correctly called for #working-constant-handler."""
self._log(f"--> SUCCESS: Handler for multiple selectors called for #{event.control.id}!")
if __name__ == "__main__":
app = BugApp()
app.run()It will be helpful if you run the following command and paste the results:
textual diagnose
If you don't have the textual command on your path, you may have forgotten to install the textual-dev package.
Feel free to add screenshots and / or videos. These can be very helpful!