Textualize/textual · error · OnDecoratorError

The attribute {attribute!r} can't be matched; have you added

Error message

The attribute {attribute!r} can't be matched; have you added it to {message_type.__name__}.ALLOW_SELECTOR_MATCH?

What it means

Beyond control, the @on decorator can match on named message attributes such as Input.Changed.key or Select.Changed.value by listing them as keyword arguments. Each attribute must be declared in the message class's ALLOW_SELECTOR_MATCH set (a frozenset of legal matchable field names). Passing any keyword not present in that set raises OnDecoratorError immediately when the module is imported, with a message that names both the bad attribute and the class whose ALLOW_SELECTOR_MATCH would need updating.

Source

Thrown at src/textual/_on.py:73

            matches the widget from the `control` attribute of the message.
        **kwargs: Additional selectors for other attributes of the message.
    """

    selectors: dict[str, str] = {}
    if selector is not None:
        selectors["control"] = selector
    if kwargs:
        selectors.update(kwargs)

    parsed_selectors: dict[str, tuple[SelectorSet, ...]] = {}
    for attribute, css_selector in selectors.items():
        if attribute == "control":
            if message_type.control == Message.control:
                raise OnDecoratorError(
                    "The message class must have a 'control' to match with the on decorator"
                )
        elif attribute not in message_type.ALLOW_SELECTOR_MATCH:
            raise OnDecoratorError(
                f"The attribute {attribute!r} can't be matched; have you added it to "
                + f"{message_type.__name__}.ALLOW_SELECTOR_MATCH?"
            )
        try:
            parsed_selectors[attribute] = parse_selectors(css_selector)
        except TokenError:
            raise OnDecoratorError(
                f"Unable to parse selector {css_selector!r} for {attribute}; check for syntax errors"
            ) from None

    def decorator(method: DecoratedType) -> DecoratedType:
        """Store message and selector in function attribute, return callable unaltered."""

        if not hasattr(method, "_textual_on"):
            setattr(method, "_textual_on", [])
        getattr(method, "_textual_on").append((message_type, parsed_selectors))

        return method

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Check the message class's ALLOW_SELECTOR_MATCH and use only attributes listed there
  2. For custom messages, add ALLOW_SELECTOR_MATCH = {"your_field"} (inheriting or extending the parent's set) and then use @on(MyMsg, your_field="...")
  3. If you need matching on an unsupported field, do the filtering manually inside the handler instead of in the decorator
  4. Pass the plain CSS selector positionally for the default control matching, and attribute selectors only as valid keywords

Example fix

# before
class Priority(Message):
    def __init__(self, level: str) -> None:
        super().__init__()
        self.level = level

@on(Priority, level="high")  # OnDecoratorError

# after
class Priority(Message):
    ALLOW_SELECTOR_MATCH = {"level"}
    def __init__(self, level: str) -> None:
        super().__init__()
        self.level = level

@on(Priority, level="high")
Defensive patterns

Strategy: validation

Validate before calling

def selectors_allowed(msg_type, kwargs: dict) -> bool:
    allowed = set(getattr(msg_type, "ALLOW_SELECTOR_MATCH", set()))
    return all(k in allowed or k == "control" for k in kwargs)

Type guard

def has_allow_list(msg_type: type) -> bool:
    return hasattr(msg_type, "ALLOW_SELECTOR_MATCH")

Try / catch

validate kwargs against ALLOW_SELECTOR_MATCH in a unit test importing the module; decorator errors surface at import time

Prevention

When it happens

Trigger: Writing @on(Input.Submitted, key="ctrl+s") when Input.Submitted.ALLOW_SELECTOR_MATCH does not include "key"; matching on arbitrary instance attributes of a message that were never allow-listed; using a custom message class and forgetting to add its matchable fields to ALLOW_SELECTOR_MATCH; passing the same selector string positionally instead of as the attribute keyword so it is treated as an attribute name.

Common situations: Assuming any message attribute is matchable; version drift where an attribute was renamed or removed from ALLOW_SELECTOR_MATCH in a Textual upgrade; copy-pasting a decorator from a similar message class (e.g. Changed vs Submitted) that allows different fields.

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/0a72c240b3fcd24a. Report an issue: GitHub.