handler name in classvar (#2675)

* handler name in classvar

* fix for worker handler name

* fix custom templates, event docs

* doc tweak

* doc tweak

* restore signature
This commit is contained in:
Will McGugan
2023-05-28 14:56:18 +01:00
committed by GitHub
parent ab10c7c326
commit 1ea892b062
7 changed files with 1292 additions and 1266 deletions

View File

@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- The default Widget repr no longer displays classes and pseudo-classes (to reduce noise in logs). Add them to your `__rich_repr__` method if needed. https://github.com/Textualize/textual/pull/2623
- Setting `Screen.AUTO_FOCUS` to `None` will inherit `AUTO_FOCUS` from the app instead of disabling it https://github.com/Textualize/textual/issues/2594
- Setting `Screen.AUTO_FOCUS` to `""` will disable it on the screen https://github.com/Textualize/textual/issues/2594
- Messages now have a `handler_name` class var which contains the name of the default handler method.
### Removed

View File

@@ -148,13 +148,40 @@ Most of the logic in a Textual app will be written in message handlers. Let's ex
Textual uses the following scheme to map messages classes on to a Python method.
- Start with `"on_"`.
- Add the messages' namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"`
- Add the message's namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"`.
- Add the name of the class converted from CamelCase to snake_case.
<div class="excalidraw">
--8<-- "docs/images/events/naming.excalidraw.svg"
</div>
Messages have a namespace if they are defined as a child class of a Widget.
The namespace is the name of the parent class.
For instance, the builtin `Input` class defines it's `Changed` message as follow:
```python
class Input(Widget):
...
class Changed(Message):
"""Posted when the value changes."""
...
```
Because `Changed` is a *child* class of `Input`, its namespace will be "input" (and the handler name will be `on_input_changed`).
This allows you to have similarly named events, without clashing event handler names.
!!! tip
If you are ever in doubt about what the handler name should be for a given event, print the `handler_name` class variable for your event class.
Here's how you would check the handler name for the `Input.Changed` event:
```py
>>> from textual.widgets import Input
>>> Input.Changed.handler_name
'on_input_changed'
```
### On decorator
In addition to the naming convention, message handlers may be created with the [`on`][textual.on] decorator, which turns a method into a handler for the given message or event.

2497
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -59,6 +59,7 @@ mypy = "^1.0.0"
pytest-cov = "^2.12.1"
mkdocs = "^1.3.0"
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
mkdocstrings-python = "0.10.1"
mkdocs-material = "^9.0.11"
mkdocs-exclude = "^1.0.2"
pre-commit = "^2.13.0"

View File

@@ -29,7 +29,6 @@ class Message:
"_forwarded",
"_no_default_action",
"_stop_propagation",
"_handler_name",
"_prevent",
]
@@ -42,6 +41,8 @@ class Message:
verbose: ClassVar[bool] = False # Message is verbose
no_dispatch: ClassVar[bool] = False # Message may not be handled by client code
namespace: ClassVar[str] = "" # Namespace to disambiguate messages
handler_name: ClassVar[str]
"""Name of the default message handler."""
def __init__(self) -> None:
self.__post_init__()
@@ -53,10 +54,6 @@ class Message:
self._forwarded = False
self._no_default_action = False
self._stop_propagation = False
name = camel_to_snake(self.__class__.__name__)
self._handler_name = (
f"on_{self.namespace}_{name}" if self.namespace else f"on_{name}"
)
self._prevent: set[type[Message]] = set()
def __rich_repr__(self) -> rich.repr.Result:
@@ -77,6 +74,8 @@ class Message:
cls.no_dispatch = no_dispatch
if namespace is not None:
cls.namespace = namespace
name = camel_to_snake(cls.__name__)
cls.handler_name = f"on_{namespace}_{name}" if namespace else f"on_{name}"
@property
def control(self) -> Widget | None:
@@ -88,12 +87,6 @@ class Message:
"""Has the message been forwarded?"""
return self._forwarded
@property
def handler_name(self) -> str:
"""The name of the handler associated with this message."""
# Property to make it read only
return self._handler_name
def _set_forwarded(self) -> None:
"""Mark this event as being forwarded."""
self._forwarded = True

View File

@@ -71,8 +71,13 @@ class _MessagePumpMeta(type):
for message_type, selectors in getattr(value, "_textual_on"):
handlers.setdefault(message_type, []).append((value, selectors))
if isclass(value) and issubclass(value, Message):
if "namespace" not in value.__dict__:
value.namespace = namespace
if "namespace" in value.__dict__:
value.handler_name = f"on_{value.__dict__['namespace']}_{camel_to_snake(value.__name__)}"
else:
value.handler_name = (
f"on_{namespace}_{camel_to_snake(value.__name__)}"
)
class_obj = super().__new__(cls, name, bases, class_dict, **kwargs)
return class_obj
@@ -616,7 +621,7 @@ class MessagePump(metaclass=_MessagePumpMeta):
message: A Message object.
"""
_rich_traceback_guard = True
handler_name = message._handler_name
handler_name = message.handler_name
# Look through the MRO to find a handler
dispatched = False

View File

@@ -117,11 +117,9 @@ class Worker(Generic[ResultType]):
"""A class to manage concurrent work (either a task or a thread)."""
@rich.repr.auto
class StateChanged(Message, bubble=False):
class StateChanged(Message, bubble=False, namespace="worker"):
"""The worker state changed."""
namespace = "worker"
def __init__(self, worker: Worker, state: WorkerState) -> None:
"""Initialize the StateChanged message.