Repository: Textualize/textual

Issue:
Investigate an improvement to how the namespace of messages is calculated
Consider the following code, where there is a base class for two widgets that share a *lot* in common but have slightly different uses/APIs/reasons-to-exist. They both have a `value` in common and so will have a `Changed` message (consider this almost pseudocode to save me trying out *all* of the details):

```python
class MyBaseWidget( Widget ):

    value: reactive[bool] = reactive( False )

    class Changed( Message ):
        pass

    def watch_value( self ) -> None:
        self.post_message_no_wait( self.Changed() )

class HelloWidget( MyBaseWidget ):
    ...

class GoodbyeWidget( MyBaseWidget ):
   ...
```

To allow for sharing the bulk of the `Changed` message code between the two child classes, it would be useful if they could be declared something like this:

```python
class HelloWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        pass

class GoodbyeWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        pass
```

This almost works perfectly, expect that the message handlers that are looked for end up always being called `on_my_base_widget_changed` when we would want them to be `on_hello_widget_changed` and `on_goodbye_widget_changed`. This can be achieved and works perfectly if you do the following:

```python
class HelloWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        namespace = "hello_widget"

class GoodbyeWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        namespace = "goodbye_widget"
```

With this setup the parent widget can be "internal" and do almost all of the work and the two child classes can just handle their own differences *and* the events end up with the right names.

So... so far so good, it works fine, there's no issue here.

However, in conversation about this with Will, we believe that the Textual internals that work out the namespace for a given message *should* get the above right without the need to declare the `namespace`.

So the purpose of this issue is, at some point, to dive into [this code or thereabouts](https://github.com/Textualize/textual/blob/ba6c8afac6ffb78dffeab9ebdc7c115fd28cd40b/src/textual/message_pump.py#L54-L61) and see if there's a reason why the `namespace` isn't worked out as we'd expect here.
Investigate an improvement to how the namespace of messages is calculated
Consider the following code, where there is a base class for two widgets that share a *lot* in common but have slightly different uses/APIs/reasons-to-exist. They both have a `value` in common and so will have a `Changed` message (consider this almost pseudocode to save me trying out *all* of the details):

```python
class MyBaseWidget( Widget ):

    value: reactive[bool] = reactive( False )

    class Changed( Message ):
        pass

    def watch_value( self ) -> None:
        self.post_message_no_wait( self.Changed() )

class HelloWidget( MyBaseWidget ):
    ...

class GoodbyeWidget( MyBaseWidget ):
   ...
```

To allow for sharing the bulk of the `Changed` message code between the two child classes, it would be useful if they could be declared something like this:

```python
class HelloWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        pass

class GoodbyeWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        pass
```

This almost works perfectly, expect that the message handlers that are looked for end up always being called `on_my_base_widget_changed` when we would want them to be `on_hello_widget_changed` and `on_goodbye_widget_changed`. This can be achieved and works perfectly if you do the following:

```python
class HelloWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        namespace = "hello_widget"

class GoodbyeWidget( MyBaseWidget ):
    class Changed( MyBaseWidget.Changed ):
        namespace = "goodbye_widget"
```

With this setup the parent widget can be "internal" and do almost all of the work and the two child classes can just handle their own differences *and* the events end up with the right names.

So... so far so good, it works fine, there's no issue here.

However, in conversation about this with Will, we believe that the Textual internals that work out the namespace for a given message *should* get the above right without the need to declare the `namespace`.

So the purpose of this issue is, at some point, to dive into [this code or thereabouts](https://github.com/Textualize/textual/blob/ba6c8afac6ffb78dffeab9ebdc7c115fd28cd40b/src/textual/message_pump.py#L54-L61) and see if there's a reason why the `namespace` isn't worked out as we'd expect here.


Discussion:
Thanks for the thorough message and pseudocode! You made finding the issue and the fix a breeze! PR incoming. 🚀 
Thanks for the thorough message and pseudocode! You made finding the issue and the fix a breeze! PR incoming. 🚀 

Resolving patch:
diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py
--- a/src/textual/message_pump.py
+++ b/src/textual/message_pump.py
@@ -62,7 +62,7 @@ def __new__(
         isclass = inspect.isclass
         for value in class_dict.values():
             if isclass(value) and issubclass(value, Message):
-                if not value.namespace:
+                if "namespace" not in value.__dict__:
                     value.namespace = namespace
         class_obj = super().__new__(cls, name, bases, class_dict, **kwargs)
         return class_obj


Test patch:
diff --git a/tests/test_message_handling.py b/tests/test_message_handling.py
new file mode 100644
--- /dev/null
+++ b/tests/test_message_handling.py
@@ -0,0 +1,45 @@
+from textual.app import App, ComposeResult
+from textual.message import Message
+from textual.widget import Widget
+
+
+async def test_message_inheritance_namespace():
+    """Inherited messages get their correct namespaces.
+
+    Regression test for https://github.com/Textualize/textual/issues/1814.
+    """
+
+    class BaseWidget(Widget):
+        class Fired(Message):
+            pass
+
+        def trigger(self) -> None:
+            self.post_message(self.Fired())
+
+    class Left(BaseWidget):
+        class Fired(BaseWidget.Fired):
+            pass
+
+    class Right(BaseWidget):
+        class Fired(BaseWidget.Fired):
+            pass
+
+    handlers_called = []
+
+    class MessageInheritanceApp(App[None]):
+        def compose(self) -> ComposeResult:
+            yield Left()
+            yield Right()
+
+        def on_left_fired(self):
+            handlers_called.append("left")
+
+        def on_right_fired(self):
+            handlers_called.append("right")
+
+    app = MessageInheritanceApp()
+    async with app.run_test():
+        app.query_one(Left).trigger()
+        app.query_one(Right).trigger()
+
+    assert handlers_called == ["left", "right"]