ai-station/.venv/lib/python3.12/site-packages/textual/_on.py

94 lines
3.3 KiB
Python

from __future__ import annotations
from typing import Callable, TypeVar
from textual.css.model import SelectorSet
from textual.css.parse import parse_selectors
from textual.css.tokenizer import TokenError
from textual.message import Message
DecoratedType = TypeVar("DecoratedType")
class OnDecoratorError(Exception):
"""Errors related to the `on` decorator.
Typically raised at import time as an early warning system.
"""
class OnNoWidget(Exception):
"""A selector was applied to an attribute that isn't a widget."""
def on(
message_type: type[Message], selector: str | None = None, **kwargs: str
) -> Callable[[DecoratedType], DecoratedType]:
"""Decorator to declare that the method is a message handler.
The decorator accepts an optional CSS selector that will be matched against a widget exposed by
a `control` property on the message.
Example:
```python
# Handle the press of buttons with ID "#quit".
@on(Button.Pressed, "#quit")
def quit_button(self) -> None:
self.app.quit()
```
Keyword arguments can be used to match additional selectors for attributes
listed in [`ALLOW_SELECTOR_MATCH`][textual.message.Message.ALLOW_SELECTOR_MATCH].
Example:
```python
# Handle the activation of the tab "#home" within the `TabbedContent` "#tabs".
@on(TabbedContent.TabActivated, "#tabs", pane="#home")
def switch_to_home(self) -> None:
self.log("Switching back to the home tab.")
...
```
Args:
message_type: The message type (i.e. the class).
selector: An optional [selector](/guide/CSS#selectors). If supplied, the handler will only be called if `selector`
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
return decorator