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.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66273dcfe..49d764f65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
+
+## Unreleased
+
+### Fixed
+
+- Fixed how the namespace for messages is calculated to facilitate inheriting messages https://github.com/Textualize/textual/issues/1814
+
 ## [0.15.0] - 2023-03-13
 
 ### Fixed
diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py
index 3a1ac9eb2..0ce2b981b 100644
--- a/src/textual/message_pump.py
+++ b/src/textual/message_pump.py
@@ -62,7 +62,7 @@ class MessagePumpMeta(type):
         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
index 000000000..31edeb2cf
--- /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"]