internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: Textualize/textual

Issue:
Pushing a screen should send Leave message
If you have an action that opens a screen, it leaves the footer stuck in the highlight state.

I think we need to call `_set_mouse_over(None)` on the current screen when pushing another screen.


Discussion:
I can't repro this.
Do you have Trogon? Run one of the examples and click the footer. When the modal returns the footer is stuck in a highlight state.

I suspect you could reproduce this will any app with a footer that launches a modal.
> I suspect you could reproduce this will any app with a footer that launches a modal.

That's it. The original issue wasn't explicit and I was using regular screens. I can repro it when pushing a modal.

Resolving patch:
diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py
--- a/src/textual/widgets/_footer.py
+++ b/src/textual/widgets/_footer.py
@@ -79,8 +79,7 @@ def _on_mouse_move(self, event: events.MouseMove) -> None:
 
     def _on_leave(self, _: events.Leave) -> None:
         """Clear any highlight when the mouse leaves the widget"""
-        if self.screen.is_current:
-            self.highlight_key = None
+        self.highlight_key = None
 
     def __rich_repr__(self) -> rich.repr.Result:
         yield from super().__rich_repr__()


Test patch:
diff --git a/tests/test_footer.py b/tests/test_footer.py
new file mode 100644
--- /dev/null
+++ b/tests/test_footer.py
@@ -0,0 +1,28 @@
+from textual.app import App, ComposeResult
+from textual.geometry import Offset
+from textual.screen import ModalScreen
+from textual.widgets import Footer, Label
+
+
+async def test_footer_highlight_when_pushing_modal():
+    """Regression test for https://github.com/Textualize/textual/issues/2606"""
+
+    class MyModalScreen(ModalScreen):
+        def compose(self) -> ComposeResult:
+            yield Label("apple")
+
+    class MyApp(App[None]):
+        BINDINGS = [("a", "p", "push")]
+
+        def compose(self) -> ComposeResult:
+            yield Footer()
+
+        def action_p(self):
+            self.push_screen(MyModalScreen())
+
+    app = MyApp()
+    async with app.run_test(size=(80, 2)) as pilot:
+        await pilot.hover(None, Offset(0, 1))
+        await pilot.click(None, Offset(0, 1))
+        assert isinstance(app.screen, MyModalScreen)
+        assert app.screen_stack[0].query_one(Footer).highlight_key is None
Repository: Textualize/textual

Issue:
Docs for `ProgressBar.percentage` talk about a `ProgressBar.Started` message that doesn't exist
Likely a docstring hangover from the evolution of `ProgressBar`?

Ping @rodrigogiraoserrao for obvious reasons.


Discussion:

Resolving patch:
diff --git a/src/textual/widgets/_progress_bar.py b/src/textual/widgets/_progress_bar.py
--- a/src/textual/widgets/_progress_bar.py
+++ b/src/textual/widgets/_progress_bar.py
@@ -281,9 +281,6 @@ class ProgressBar(Widget, can_focus=False):
 
     The percentage is a value between 0 and 1 and the returned value is only
     `None` if the total progress of the bar hasn't been set yet.
-    In other words, after the progress bar emits the message
-    [`ProgressBar.Started`][textual.widgets.ProgressBar.Started],
-    the value of `percentage` is always not `None`.
 
     Example:
         ```py


Test patch:
Repository: Textualize/textual

Issue:
It looks like `on_hide` is not called when a widget is hidden in some way
I would expect the following application to crash when the button is pressed:

```python
from textual.app        import App, ComposeResult
from textual.containers import Vertical
from textual.widgets    import Button, Label

class ShouldCrash( Label ):

    def on_hide( self ) -> None:
        _ = 1/0

class NoHideApp( App[ None ] ):

    CSS = """
    Vertical {
        align: center middle;
    }
    """

    def compose( self ) -> ComposeResult:
        with Vertical():
            yield Button( "Press to hide the label below" )
            yield ShouldCrash( "Press the button above me to hide me" )

    def on_button_pressed( self ):
        self.query_one( ShouldCrash ).display = False

if __name__ == "__main__":
    NoHideApp().run()
```

However, when the button is pressed; it doesn't. It would appear that the `Hide` message isn't being sent. Looking in the debug console I don't see the event.

The same is true if I attempt to hide the widget using any of:

```python
self.query_one( ShouldCrash ).display = "none"
self.query_one( ShouldCrash ).visible = False
self.query_one( ShouldCrash ).set_class( True, "hidden" ) # With appropriate CSS to back it up.
```


Discussion:
I may have this wrong, but I think the issue might be in [`_compositor.reflow()`](https://github.com/Textualize/textual/blob/main/src/textual/_compositor.py#L366-L395).

It looks like `self.widgets = widgets` and then `hidden_widgets = self.widgets - widgets` would mean `hidden_widgets` is always empty?

Should it maybe be `hidden_widgets = old_widgets - new_widgets` instead?
It looks like this was introduced [here](https://github.com/Textualize/textual/commit/11d10db1ab9cc723a5f12bf333d22f9b00d5e9a3#diff-3680fc293baa11cb6d4fd3f21fe9b3b0c16c36131ce36524349c1c00e2a85ca2L267-L271), where `hidden_widgets` was set before `self.widgets = widgets`.

Resolving patch:
diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py
--- a/src/textual/_compositor.py
+++ b/src/textual/_compositor.py
@@ -361,6 +361,11 @@ def reflow(self, parent: Widget, size: Size) -> ReflowResult:
 
         new_widgets = map.keys()
 
+        # Newly visible widgets
+        shown_widgets = new_widgets - old_widgets
+        # Newly hidden widgets
+        hidden_widgets = self.widgets - widgets
+
         # Replace map and widgets
         self._full_map = map
         self.widgets = widgets
@@ -389,10 +394,7 @@ def reflow(self, parent: Widget, size: Size) -> ReflowResult:
             for widget, (region, *_) in changes
             if (widget in common_widgets and old_map[widget].region[2:] != region[2:])
         }
-        # Newly visible widgets
-        shown_widgets = new_widgets - old_widgets
-        # Newly hidden widgets
-        hidden_widgets = self.widgets - widgets
+
         return ReflowResult(
             hidden=hidden_widgets,
             shown=shown_widgets,


Test patch:
Repository: Textualize/textual

Issue:
Remove `Reactive.var` in favour of `var`?
In `reactive.py` we have the classmethod `Reactive.var` and the class `var` that subclasses `Reactive`.
They are not strictly equivalent, but they seem to have the same purpose.
Should we delete `Reactive.var`?


Discussion:

Resolving patch:
diff --git a/src/textual/reactive.py b/src/textual/reactive.py
--- a/src/textual/reactive.py
+++ b/src/textual/reactive.py
@@ -72,23 +72,6 @@ def __rich_repr__(self) -> rich.repr.Result:
         yield "always_update", self._always_update
         yield "compute", self._run_compute
 
-    @classmethod
-    def var(
-        cls,
-        default: ReactiveType | Callable[[], ReactiveType],
-        always_update: bool = False,
-    ) -> Reactive:
-        """A reactive variable that doesn't update or layout.
-
-        Args:
-            default: A default value or callable that returns a default.
-            always_update: Call watchers even when the new value equals the old value.
-
-        Returns:
-            A Reactive descriptor.
-        """
-        return cls(default, layout=False, repaint=False, init=False)
-
     def _initialize_reactive(self, obj: Reactable, name: str) -> None:
         """Initialized a reactive attribute on an object.
 


Test patch:
Repository: Textualize/textual

Issue:
Updating a `TreeNode.label` doesn't refresh the tree right away
In this little illustration of how a `Tree`'s nodes' labels can be updated on the fly, the actual updates don't show until some other interaction with the `Tree` takes place (moving the cursor, causing a mouse hover event, etc):

```python
from textual.app     import App, ComposeResult
from textual.widgets import Header, Footer, Tree

class TreeNodeUpdateApp( App[ None ] ):

    BINDINGS = [
        ( "a", "add", "" ),
    ]

    def compose( self ) -> ComposeResult:
        yield Header()
        yield Tree( "100" )
        yield Footer()

    def on_mount( self ) -> None:
        for n in range( 10 ):
            node = self.query_one( Tree ).root.add( str( n ), expand=True )
            for m in range( 10 ):
                node.add_leaf( str( m ) )

    def action_add( self ):
        node = self.query_one( Tree ).cursor_node
        node.label = str( int( str( node.label ) ) + 1 )

if __name__ == "__main__":
    TreeNodeUpdateApp().run()
```

adding a `self.query_one( Tree ).refresh()` to the end of `action_add` fixes it so the update of the label is reflected right away.

[`TreeNode.set_label`](https://github.com/Textualize/textual/blob/149c39c86c083772d3249c8bbd5c1fa7923a8d55/src/textual/widgets/_tree.py#L301-L309) should probably be changed to cause the `Tree` to perform a `refresh`.


Discussion:

Resolving patch:
diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py
--- a/src/textual/widgets/_tree.py
+++ b/src/textual/widgets/_tree.py
@@ -307,6 +307,7 @@ def set_label(self, label: TextType) -> None:
         self._updates += 1
         text_label = self._tree.process_label(label)
         self._label = text_label
+        self._tree.call_later(self._tree._refresh_node, self)
 
     def add(
         self,


Test patch:
Repository: Textualize/textual

Issue:
docs: `Message` not exported from `textual.messages`
Just something minor I spotted while playing with the [Messages up](https://textual.textualize.io/guide/widgets/#messages-up) example in the docs. The code still works but my editor complains that:

> "Message" is not exported from module "textual.messages"
> Import from "textual.message" instead [reportPrivateImportUsage]

Happy to submit a quick PR if deemed worth fixing!


Discussion:
Good catch. PR would be great!

Resolving patch:
diff --git a/docs/examples/guide/compound/byte02.py b/docs/examples/guide/compound/byte02.py
--- a/docs/examples/guide/compound/byte02.py
+++ b/docs/examples/guide/compound/byte02.py
@@ -2,7 +2,7 @@
 
 from textual.app import App, ComposeResult
 from textual.containers import Container
-from textual.messages import Message
+from textual.message import Message
 from textual.reactive import reactive
 from textual.widget import Widget
 from textual.widgets import Input, Label, Switch
diff --git a/docs/examples/guide/compound/byte03.py b/docs/examples/guide/compound/byte03.py
--- a/docs/examples/guide/compound/byte03.py
+++ b/docs/examples/guide/compound/byte03.py
@@ -3,7 +3,7 @@
 from textual.app import App, ComposeResult
 from textual.containers import Container
 from textual.geometry import clamp
-from textual.messages import Message
+from textual.message import Message
 from textual.reactive import reactive
 from textual.widget import Widget
 from textual.widgets import Input, Label, Switch


Test patch:
Repository: Textualize/textual

Issue:
A lone `Static` results in a `TooManyMatches` error when using `query_one`
I've not dived into this beyond knocking up this example to isolate what I saw (about to head out of the door but wanted to record this as a reminder). With 0.27.0 (perhaps before too, just noting the version here for the record), this code:

```python
from textual.app     import App, ComposeResult
from textual.widgets import Static

class OneStatic( App[ None ] ):

    def compose( self ) -> ComposeResult:
        yield Static()

    def on_mount( self ) -> None:
        self.query_one( Static ).update( "Hello, World!" )

if __name__ == "__main__":
    OneStatic().run()
```

results in a `TooManyMatches` error being raised from the `query_one`. With very early testing this only seems to be the case with `Static` (at least, I tested with `Label` and `Button` and they're fine).

I think most people would rightly find this surprising.


Discussion:
Looks like the problem is that the new `Tooltip` is mounted which inherits from `Static`?
Ahh. Good call. Might have to think about how to do things like tooltips without being visible in the DOM.

Resolving patch:
diff --git a/src/textual/widgets/_tooltip.py b/src/textual/widgets/_tooltip.py
--- a/src/textual/widgets/_tooltip.py
+++ b/src/textual/widgets/_tooltip.py
@@ -3,7 +3,7 @@
 from textual.widgets import Static
 
 
-class Tooltip(Static):
+class Tooltip(Static, inherit_css=False):
     DEFAULT_CSS = """
     Tooltip {
         layer: _tooltips;


Test patch:
Repository: Textualize/textual

Issue:
Textual interferes with other libraries' use of logging
I'm trying to create a Textual app that creates an instance of an object that eventually loads the [`transformers` module](https://pypi.org/project/transformers/), and this fails due to something that module is doing with the logging module. Here's a minimal example that demonstrates the issue:

```python
import logging, sys
from textual.app import App
from textual.logging import TextualHandler

logging.basicConfig(
    level="NOTSET",
    handlers=[TextualHandler()],
)

class OtherClass:
    def __init__(self):
        # https://github.com/huggingface/transformers/blob/main/src/transformers/utils/logging.py#L87
        _default_handler = logging.StreamHandler()  # Set sys.stderr as stream.                                                        │
        _default_handler.flush = sys.stderr.flush

class MyApp(App):
    def on_mount(self) -> None:
        self.other = OtherClass()

if __name__ == "__main__":
    app = MyApp()
    app.run()
```

This fails with the error `AttributeError: 'NoneType' object has no attribute 'flush'`.

`TextualHandler` doesn't alter this behavior. The workaround I'm currently using is defining the instance of OtherClass in the main, before creating MyApp, but that does make some of the code a lot more cumbersome and it shouldn't be necessary.


Discussion:

Resolving patch:
diff --git a/src/textual/app.py b/src/textual/app.py
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -1978,8 +1978,10 @@ async def invoke_ready_callback() -> None:
                                 with redirect_stdout(redirector):  # type: ignore
                                     await run_process_messages()
                         else:
-                            with redirect_stderr(None):
-                                with redirect_stdout(None):
+                            with open(os.devnull, "w") as null_file:
+                                with redirect_stdout(null_file), redirect_stderr(
+                                    null_file
+                                ):
                                     await run_process_messages()
 
                 finally:


Test patch:
Repository: Textualize/textual

Issue:
`Switch` should stop the `Click` event from bubbling
At the moment `Switch` handles `Click` but then lets it bubble; there's no good reason to do that and it also stops the ability to write something like this:

```python
from textual.app        import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets    import Header, Footer, Label, Switch

class LabeledSwitch( Horizontal ):

    def on_click( self ) -> None:
        self.query_one(Switch).toggle()

class ClickableLabelApp( App[ None ] ):

    def compose( self ) -> ComposeResult:
        yield Header()
        with LabeledSwitch():
            yield Label( "Click me!" )
            yield Switch()
        yield Footer()

if __name__ == "__main__":
    ClickableLabelApp().run()
```

where the idea is to make a compound widget that lets you click on the `Label` or the `Switch` and the `Switch` will toggle -- only it doesn't work if you click on the `Switch` because it ends up double-toggling.


Discussion:
(This issue is reserved for the [EuroPython 2023 sprint](https://ep2023.europython.eu/sprints). If you are not participating in the sprint, please refrain from working on this issue. Thanks!)

@davep 's explanation of what we want to achieve is pretty clear, provided [you know what bubbling means](https://textual.textualize.io/guide/events/#bubbling).
This should be a simple fix.
You can even use Dave's example app, above, in your tests for your fix.

Good luck and have fun!
I'd like to have a go at this :)

Resolving patch:
diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py
--- a/src/textual/widgets/_switch.py
+++ b/src/textual/widgets/_switch.py
@@ -154,8 +154,9 @@ def get_content_width(self, container: Size, viewport: Size) -> int:
     def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
         return 1
 
-    async def _on_click(self, _: Click) -> None:
+    async def _on_click(self, event: Click) -> None:
         """Toggle the state of the switch."""
+        event.stop()
         self.toggle()
 
     def action_toggle(self) -> None:


Test patch:
diff --git a/tests/test_switch.py b/tests/test_switch.py
new file mode 100644
--- /dev/null
+++ b/tests/test_switch.py
@@ -0,0 +1,18 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Switch
+
+
+async def test_switch_click_doesnt_bubble_up():
+    """Regression test for https://github.com/Textualize/textual/issues/2366"""
+
+    class SwitchApp(App[None]):
+        def compose(self) -> ComposeResult:
+            yield Switch()
+
+        async def on_click(self) -> None:
+            raise AssertionError(
+                "The app should never receive a click event when Switch is clicked."
+            )
+
+    async with SwitchApp().run_test() as pilot:
+        await pilot.click(Switch)
Repository: Textualize/textual

Issue:
Add `action_switch_mode`
Following on from #2327 it would seem to make sense to add a `switch_mode` `action_` method, much like the various `Screen`-oriented `App` methods also have `action_` wrappers.


Discussion:

Resolving patch:
diff --git a/src/textual/app.py b/src/textual/app.py
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -2792,6 +2792,10 @@ async def action_pop_screen(self) -> None:
         """An [action](/guide/actions) to remove the topmost screen and makes the new topmost screen active."""
         self.pop_screen()
 
+    async def action_switch_mode(self, mode: str) -> None:
+        """An [action](/guide/actions) that switches to the given mode.."""
+        self.switch_mode(mode)
+
     async def action_back(self) -> None:
         """An [action](/guide/actions) to go back to the previous screen (pop the current screen).
 


Test patch:
Repository: Textualize/textual

Issue:
ListView method to append many
There is a method on ListView to append a single item. We could also use a method to append many items.

If we're following the same conventions as a builtin list, this would be called `ListView.extend` and would accept an Iterable of ListItems.


Discussion:

Resolving patch:
diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py
--- a/src/textual/widgets/_list_view.py
+++ b/src/textual/widgets/_list_view.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import ClassVar, Optional
+from typing import ClassVar, Iterable, Optional
 
 from textual.await_remove import AwaitRemove
 from textual.binding import Binding, BindingType
@@ -172,6 +172,21 @@ def watch_index(self, old_index: int, new_index: int) -> None:
         self._scroll_highlighted_region()
         self.post_message(self.Highlighted(self, new_child))
 
+    def extend(self, items: Iterable[ListItem]) -> AwaitMount:
+        """Append multiple new ListItems to the end of the ListView.
+
+        Args:
+            items: The ListItems to append.
+
+        Returns:
+            An awaitable that yields control to the event loop
+                until the DOM has been updated with the new child items.
+        """
+        await_mount = self.mount(*items)
+        if len(self) == 1:
+            self.index = 0
+        return await_mount
+
     def append(self, item: ListItem) -> AwaitMount:
         """Append a new ListItem to the end of the ListView.
 
@@ -182,10 +197,7 @@ def append(self, item: ListItem) -> AwaitMount:
             An awaitable that yields control to the event loop
                 until the DOM has been updated with the new child item.
         """
-        await_mount = self.mount(item)
-        if len(self) == 1:
-            self.index = 0
-        return await_mount
+        return self.extend([item])
 
     def clear(self) -> AwaitRemove:
         """Clear all items from the ListView.


Test patch:
Repository: Textualize/textual

Issue:
Binding for `]` renders incorrectly in `Footer`
The binding `Binding("ctrl+right_square_bracket", "toggle_indent_width", "Cycle indent width")` renders like this:

<img width="431" alt="image" src="https://github.com/Textualize/textual/assets/5740731/2c2bd6fa-288b-4205-aba0-48eb1b6c41e0">

It should probably render as `Ctrl+]`.


Discussion:
Just to add that this isn't specific to the `right_square_bracket` key. 

For example, the binding for `ctrl+left` will also display the text rather than the symbol:

```python
from textual.app import App, ComposeResult
from textual.widgets import Footer


class ExampleApp(App):
    BINDINGS = [
        ("left", "left", "Symbol"),
        ("ctrl+left", "ctrl_left", "No Symbol"),
        ("right_square_bracket", "right_square_bracket", "Symbol"),
        ("ctrl+right_square_bracket", "ctrl_right_square_bracket", "No Symbol"),
    ]

    def compose(self) -> ComposeResult:
        yield Footer()

    def action_left(self) -> None:
        self.bell()

    def action_ctrl_left(self) -> None:
        self.bell()

    def action_right_square_bracket(self) -> None:
        self.bell()

    def action_ctrl_right_square_bracket(self) -> None:
        self.bell()


if __name__ == "__main__":
    app = ExampleApp()
    app.run()
```

Resolving patch:
diff --git a/src/textual/keys.py b/src/textual/keys.py
--- a/src/textual/keys.py
+++ b/src/textual/keys.py
@@ -283,6 +283,9 @@ def _get_key_display(key: str) -> str:
     """Given a key (i.e. the `key` string argument to Binding __init__),
     return the value that should be displayed in the app when referring
     to this key (e.g. in the Footer widget)."""
+    if "+" in key:
+        return "+".join([_get_key_display(key) for key in key.split("+")])
+
     display_alias = KEY_DISPLAY_ALIASES.get(key)
     if display_alias:
         return display_alias


Test patch:
diff --git a/tests/test_keys.py b/tests/test_keys.py
--- a/tests/test_keys.py
+++ b/tests/test_keys.py
@@ -52,3 +52,20 @@ def action_increment(self) -> None:
 
 def test_get_key_display():
     assert _get_key_display("minus") == "-"
+
+
+def test_get_key_display_when_used_in_conjunction():
+    """Test a key display is the same if used in conjunction with another key.
+    For example, "ctrl+right_square_bracket" should display the bracket as "]",
+    the same as it would without the ctrl modifier.
+
+    Regression test for #3035 https://github.com/Textualize/textual/issues/3035
+    """
+
+    right_square_bracket = _get_key_display("right_square_bracket")
+    ctrl_right_square_bracket = _get_key_display("ctrl+right_square_bracket")
+    assert ctrl_right_square_bracket == f"CTRL+{right_square_bracket}"
+
+    left = _get_key_display("left")
+    ctrl_left = _get_key_display("ctrl+left")
+    assert ctrl_left == f"CTRL+{left}"