Skip to content

Commit c9bb137

Browse files
arvidfmdarrenburns
andauthored
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 <[email protected]> Co-authored-by: Darren Burns <[email protected]>
1 parent 433d78f commit c9bb137

File tree

4 files changed

+16
-13
lines changed

4 files changed

+16
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2929
- Fixed `Tree` and `DirectoryTree` horizontal scrolling off-by-2 https://github.com/Textualize/textual/pull/4744
3030
- Fixed text-opacity in component styles https://github.com/Textualize/textual/pull/4747
3131
- Ensure `Tree.select_node` sends `NodeSelected` message https://github.com/Textualize/textual/pull/4753
32+
- Fixed message handlers not working when message types are assigned as the value of class vars https://github.com/Textualize/textual/pull/3940
3233
- Fixed `CommandPalette` not focusing the input when opened when `App.AUTO_FOCUS` doesn't match the input https://github.com/Textualize/textual/pull/4763
3334
- `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
3435

src/textual/message.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,16 @@ def __init_subclass__(
7474
cls.no_dispatch = no_dispatch
7575
if namespace is not None:
7676
cls.namespace = namespace
77-
name = camel_to_snake(cls.__name__)
78-
cls.handler_name = f"on_{namespace}_{name}" if namespace else f"on_{name}"
77+
name = f"{namespace}_{camel_to_snake(cls.__name__)}"
78+
else:
79+
# a class defined inside of a function will have a qualified name like func.<locals>.Class,
80+
# so make sure we only use the actual class name(s)
81+
qualname = cls.__qualname__.rsplit("<locals>.", 1)[-1]
82+
# only keep the last two parts of the qualified name of deeply nested classes
83+
# for backwards compatibility, e.g. A.B.C.D becomes C.D
84+
namespace = qualname.rsplit(".", 2)[-2:]
85+
name = "_".join(camel_to_snake(part) for part in namespace)
86+
cls.handler_name = f"on_{name}"
7987

8088
@property
8189
def control(self) -> DOMNode | None:

src/textual/message_pump.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from __future__ import annotations
1212

1313
import asyncio
14-
import inspect
1514
import threading
1615
from asyncio import CancelledError, Queue, QueueEmpty, Task, create_task
1716
from contextlib import contextmanager
@@ -36,7 +35,6 @@
3635
from ._context import prevent_message_types_stack
3736
from ._on import OnNoWidget
3837
from ._time import time
39-
from .case import camel_to_snake
4038
from .css.match import match
4139
from .errors import DuplicateKeyHandlers
4240
from .events import Event
@@ -78,8 +76,6 @@ def __new__(
7876
class_dict: dict[str, Any],
7977
**kwargs: Any,
8078
) -> _MessagePumpMetaSub:
81-
namespace = camel_to_snake(name)
82-
isclass = inspect.isclass
8379
handlers: dict[
8480
type[Message], list[tuple[Callable, dict[str, tuple[SelectorSet, ...]]]]
8581
] = class_dict.get("_decorated_handlers", {})
@@ -93,13 +89,6 @@ def __new__(
9389
] = getattr(value, "_textual_on")
9490
for message_type, selectors in textual_on:
9591
handlers.setdefault(message_type, []).append((value, selectors))
96-
if isclass(value) and issubclass(value, Message):
97-
if "namespace" in value.__dict__:
98-
value.handler_name = f"on_{value.__dict__['namespace']}_{camel_to_snake(value.__name__)}"
99-
else:
100-
value.handler_name = (
101-
f"on_{namespace}_{camel_to_snake(value.__name__)}"
102-
)
10392

10493
# Look for reactives with public AND private compute methods.
10594
prefix = "compute_"

tests/test_message_handling.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class Right(BaseWidget):
2424
class Fired(BaseWidget.Fired):
2525
pass
2626

27+
class DummyWidget(Widget):
28+
# ensure that referencing a message type in other class scopes
29+
# doesn't break the namespace
30+
_event = Left.Fired
31+
2732
handlers_called = []
2833

2934
class MessageInheritanceApp(App[None]):

0 commit comments

Comments
 (0)