Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

calculate message namespace from __qualname__ when not specified #3940

Merged
merged 10 commits into from
Jul 17, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 10 additions & 2 deletions src/textual/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.<locals>.Class,
# so make sure we only use the actual class name(s)
qualname = cls.__qualname__.rsplit("<locals>.", 1)[-1]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seem brittle. Is it guaranteed that CPython and all other Python's will have a qualname formatted like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's explicitly defined in the PEP that introduced __qualname__: https://peps.python.org/pep-3155/#proposal

# 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:
Expand Down
11 changes: 0 additions & 11 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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", {})
Expand All @@ -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_"
Expand Down
5 changes: 5 additions & 0 deletions tests/test_message_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
Loading