calculate message namespace from __qualname__ when not specified (#3940)

* use __qualname__ for the default message namespace

* improve tests

* update changelog

* better, more backwards compatible splitting

* Fix syntax

* Fix CHANGELOG

---------

Co-authored-by: Darren Burns <darrenburns@users.noreply.github.com>
Co-authored-by: Darren Burns <darrenb900@gmail.com>
This commit is contained in:
Arvid Fahlström Myrman
2024-07-17 13:38:28 +01:00
committed by GitHub
parent 433d78f270
commit c9bb137c0a
4 changed files with 16 additions and 13 deletions

View File

@@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed `Tree` and `DirectoryTree` horizontal scrolling off-by-2 https://github.com/Textualize/textual/pull/4744
- Fixed text-opacity in component styles https://github.com/Textualize/textual/pull/4747
- Ensure `Tree.select_node` sends `NodeSelected` message https://github.com/Textualize/textual/pull/4753
- Fixed message handlers not working when message types are assigned as the value of class vars https://github.com/Textualize/textual/pull/3940
- Fixed `CommandPalette` not focusing the input when opened when `App.AUTO_FOCUS` doesn't match the input https://github.com/Textualize/textual/pull/4763
- `SelectionList.SelectionToggled` will now be sent for each option when a bulk toggle is performed (e.g. `toggle_all`). Previously no messages were sent at all. https://github.com/Textualize/textual/pull/4759

View File

@@ -74,8 +74,16 @@ 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}"
name = f"{namespace}_{camel_to_snake(cls.__name__)}"
else:
# a class defined inside of a function will have a qualified name like func.<locals>.Class,
# so make sure we only use the actual class name(s)
qualname = cls.__qualname__.rsplit("<locals>.", 1)[-1]
# only keep the last two parts of the qualified name of deeply nested classes
# for backwards compatibility, e.g. A.B.C.D becomes C.D
namespace = qualname.rsplit(".", 2)[-2:]
name = "_".join(camel_to_snake(part) for part in namespace)
cls.handler_name = f"on_{name}"
@property
def control(self) -> DOMNode | None:

View File

@@ -11,7 +11,6 @@ A `MessagePump` is a base class for any object which processes messages, which i
from __future__ import annotations
import asyncio
import inspect
import threading
from asyncio import CancelledError, Queue, QueueEmpty, Task, create_task
from contextlib import contextmanager
@@ -36,7 +35,6 @@ from ._context import message_hook as message_hook_context_var
from ._context import prevent_message_types_stack
from ._on import OnNoWidget
from ._time import time
from .case import camel_to_snake
from .css.match import match
from .errors import DuplicateKeyHandlers
from .events import Event
@@ -78,8 +76,6 @@ class _MessagePumpMeta(type):
class_dict: dict[str, Any],
**kwargs: Any,
) -> _MessagePumpMetaSub:
namespace = camel_to_snake(name)
isclass = inspect.isclass
handlers: dict[
type[Message], list[tuple[Callable, dict[str, tuple[SelectorSet, ...]]]]
] = class_dict.get("_decorated_handlers", {})
@@ -93,13 +89,6 @@ class _MessagePumpMeta(type):
] = getattr(value, "_textual_on")
for message_type, selectors in textual_on:
handlers.setdefault(message_type, []).append((value, selectors))
if isclass(value) and issubclass(value, Message):
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__)}"
)
# Look for reactives with public AND private compute methods.
prefix = "compute_"

View File

@@ -24,6 +24,11 @@ async def test_message_inheritance_namespace():
class Fired(BaseWidget.Fired):
pass
class DummyWidget(Widget):
# ensure that referencing a message type in other class scopes
# doesn't break the namespace
_event = Left.Fired
handlers_called = []
class MessageInheritanceApp(App[None]):