Skip to content

@on(events.Click, css_selector) failure #6319

@mMerlin

Description

@mMerlin

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()

Image

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions