{"record":{"id":"0a72c240b3fcd24a","repo":"Textualize/textual","slug":"the-attribute-attribute-r-can-t-be-matched-have","errorCode":null,"errorMessage":"The attribute {attribute!r} can't be matched; have you added it to {message_type.__name__}.ALLOW_SELECTOR_MATCH?","messagePattern":"The attribute (.+?) can't be matched; have you added it to (.+?)\\.ALLOW_SELECTOR_MATCH\\?","errorType":"exception","errorClass":"OnDecoratorError","httpStatus":null,"severity":"error","filePath":"src/textual/_on.py","lineNumber":73,"sourceCode":"            matches the widget from the `control` attribute of the message.\n        **kwargs: Additional selectors for other attributes of the message.\n    \"\"\"\n\n    selectors: dict[str, str] = {}\n    if selector is not None:\n        selectors[\"control\"] = selector\n    if kwargs:\n        selectors.update(kwargs)\n\n    parsed_selectors: dict[str, tuple[SelectorSet, ...]] = {}\n    for attribute, css_selector in selectors.items():\n        if attribute == \"control\":\n            if message_type.control == Message.control:\n                raise OnDecoratorError(\n                    \"The message class must have a 'control' to match with the on decorator\"\n                )\n        elif attribute not in message_type.ALLOW_SELECTOR_MATCH:\n            raise OnDecoratorError(\n                f\"The attribute {attribute!r} can't be matched; have you added it to \"\n                + f\"{message_type.__name__}.ALLOW_SELECTOR_MATCH?\"\n            )\n        try:\n            parsed_selectors[attribute] = parse_selectors(css_selector)\n        except TokenError:\n            raise OnDecoratorError(\n                f\"Unable to parse selector {css_selector!r} for {attribute}; check for syntax errors\"\n            ) from None\n\n    def decorator(method: DecoratedType) -> DecoratedType:\n        \"\"\"Store message and selector in function attribute, return callable unaltered.\"\"\"\n\n        if not hasattr(method, \"_textual_on\"):\n            setattr(method, \"_textual_on\", [])\n        getattr(method, \"_textual_on\").append((message_type, parsed_selectors))\n\n        return method","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/_on.py#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the message class's ALLOW_SELECTOR_MATCH and use only attributes listed there","For custom messages, add ALLOW_SELECTOR_MATCH = {\"your_field\"} (inheriting or extending the parent's set) and then use @on(MyMsg, your_field=\"...\")","If you need matching on an unsupported field, do the filtering manually inside the handler instead of in the decorator","Pass the plain CSS selector positionally for the default control matching, and attribute selectors only as valid keywords"],"exampleFix":"# before\nclass Priority(Message):\n    def __init__(self, level: str) -> None:\n        super().__init__()\n        self.level = level\n\n@on(Priority, level=\"high\")  # OnDecoratorError\n\n# after\nclass Priority(Message):\n    ALLOW_SELECTOR_MATCH = {\"level\"}\n    def __init__(self, level: str) -> None:\n        super().__init__()\n        self.level = level\n\n@on(Priority, level=\"high\")","handlingStrategy":"validation","validationCode":"def selectors_allowed(msg_type, kwargs: dict) -> bool:\n    allowed = set(getattr(msg_type, \"ALLOW_SELECTOR_MATCH\", set()))\n    return all(k in allowed or k == \"control\" for k in kwargs)","typeGuard":"def has_allow_list(msg_type: type) -> bool:\n    return hasattr(msg_type, \"ALLOW_SELECTOR_MATCH\")","tryCatchPattern":"validate kwargs against ALLOW_SELECTOR_MATCH in a unit test importing the module; decorator errors surface at import time","preventionTips":["Read ALLOW_SELECTOR_MATCH on the message class before adding keyword selectors","Declare ALLOW_SELECTOR_MATCH on custom messages","Rename/update selectors when upgrading Textual versions; check the changelog"],"tags":["textual","decorator","event-handling","selector"],"backgroundTag":"decorator-argument-mismatch","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}