diff --git a/CHANGELOG.md b/CHANGELOG.md index ce01e8bf9a..3518374a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/textual/message.py b/src/textual/message.py index 5eee60d8fd..383eb34d33 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -74,8 +74,16 @@ def __init_subclass__( 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..Class, + # so make sure we only use the actual class name(s) + qualname = cls.__qualname__.rsplit(".", 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: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index bce43092c9..108c51b4cf 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -11,7 +11,6 @@ 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 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 @@ def __new__( 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 @@ def __new__( ] = 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_" diff --git a/tests/test_message_handling.py b/tests/test_message_handling.py index 31edeb2cf4..3215ab6deb 100644 --- a/tests/test_message_handling.py +++ b/tests/test_message_handling.py @@ -24,6 +24,11 @@ class Right(BaseWidget): 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]):