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

Issue:
"No module named 'textual.widgets._tab'" thrown when textual.widgets.Tab is referenced
Was trying out the new `Tabs` widget using Python 3.10.4 and found that I couldn't use `Tabs` together with `Tab` (needed `Tab` to assign CSS IDs). This issue can be reproduced by applying the following diff to textual 0.15.0:

```diff
diff --git a/docs/examples/widgets/tabs.py b/docs/examples/widgets/tabs.py
index 675ccf38..307f44e3 100644
--- a/docs/examples/widgets/tabs.py
+++ b/docs/examples/widgets/tabs.py
@@ -1,5 +1,5 @@
 from textual.app import App, ComposeResult
-from textual.widgets import Footer, Label, Tabs
+from textual.widgets import Footer, Label, Tabs, Tab
 
 NAMES = [
     "Paul Atreidies",
@@ -40,7 +40,7 @@ class TabsApp(App):
     ]
 
     def compose(self) -> ComposeResult:
-        yield Tabs(NAMES[0])
+        yield Tabs(Tab(NAMES[0]))
         yield Label()
         yield Footer()
```

Run `python docs/examples/widgets/tabs.py`, and I got:

```
Traceback (most recent call last):
  File "/Users/lian/local/src/textual/main/docs/examples/widgets/tabs.py", line 2, in <module>
    from textual.widgets import Footer, Label, Tabs, Tab
  File "/Users/lian/.pyenv/versions/3.10.4/lib/python3.10/site-packages/textual/widgets/__init__.py", line 80, in __getattr__
    module = import_module(widget_module_path, package="textual.widgets")
  File "/Users/lian/.pyenv/versions/3.10.4/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
ModuleNotFoundError: No module named 'textual.widgets._tab'
```


Discussion:
Good spot! Missing file/export in the widgets directory. Easy fix.

Resolving patch:
diff --git a/src/textual/widgets/_tab.py b/src/textual/widgets/_tab.py
new file mode 100644
--- /dev/null
+++ b/src/textual/widgets/_tab.py
@@ -0,0 +1,3 @@
+from ._tabs import Tab
+
+__all__ = ["Tab"]


Test patch:
Repository: Textualize/textual

Issue:
Consider updating `DirectoryTree` so that it takes `Path` as well as a `str` as the path to browse
Some people tend to favour using `Path` over `str` for paths and the like, so I feel it would be an idea to accept a `Path` as the path.


Discussion:
Thank you for your issue. Give us a little time to review it.

PS. You might want to check the [FAQ](https://github.com/textualize/textual/blob/main/FAQ.md) if you haven't done so already.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)

Resolving patch:
diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py
--- a/src/textual/widgets/_directory_tree.py
+++ b/src/textual/widgets/_directory_tree.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import os
 from dataclasses import dataclass
 from pathlib import Path
 from typing import ClassVar
@@ -7,14 +8,13 @@
 from rich.style import Style
 from rich.text import Text, TextType
 
-from .._types import MessageTarget
 from ..message import Message
 from ._tree import TOGGLE_STYLE, Tree, TreeNode
 
 
 @dataclass
 class DirEntry:
-    """Attaches directory information ot a node."""
+    """Attaches directory information to a node."""
 
     path: str
     is_dir: bool
@@ -26,9 +26,9 @@ class DirectoryTree(Tree[DirEntry]):
 
     Args:
         path: Path to directory.
-        name: The name of the widget, or None for no name. Defaults to None.
-        id: The ID of the widget in the DOM, or None for no ID. Defaults to None.
-        classes: A space-separated list of classes, or None for no classes. Defaults to None.
+        name: The name of the widget, or None for no name.
+        id: The ID of the widget in the DOM, or None for no ID.
+        classes: A space-separated list of classes, or None for no classes.
         disabled: Whether the directory tree is disabled or not.
     """
 
@@ -83,17 +83,18 @@ def __init__(self, path: str) -> None:
 
     def __init__(
         self,
-        path: str,
+        path: str | Path,
         *,
         name: str | None = None,
         id: str | None = None,
         classes: str | None = None,
         disabled: bool = False,
     ) -> None:
-        self.path = path
+        str_path = os.fspath(path)
+        self.path = str_path
         super().__init__(
-            path,
-            data=DirEntry(path, True),
+            str_path,
+            data=DirEntry(str_path, True),
             name=name,
             id=id,
             classes=classes,


Test patch:
Repository: Textualize/textual

Issue:
Tabs widget should use new scroll_to_center



Discussion:
Maybe I got the _intended semantics_ of `scroll_to_center` wrong.

`widget.scroll_to_center` will scroll `widget` to the center of the screen.
Should it be `container.scroll_to_center(widget)` so that it scrolls `widget` to the center of `container`?
Yes, we don't always want to scroll everything to the center.

Resolving patch:
diff --git a/src/textual/widget.py b/src/textual/widget.py
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -2475,6 +2475,7 @@ async def _scroll_to_center_of(
 
     def scroll_to_center(
         self,
+        widget: Widget,
         animate: bool = True,
         *,
         speed: float | None = None,
@@ -2493,8 +2494,8 @@ def scroll_to_center(
         """
 
         self.call_after_refresh(
-            self.screen._scroll_to_center_of,
-            widget=self,
+            self._scroll_to_center_of,
+            widget=widget,
             animate=animate,
             speed=speed,
             duration=duration,
diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py
--- a/src/textual/widgets/_tabs.py
+++ b/src/textual/widgets/_tabs.py
@@ -449,7 +449,7 @@ def _activate_tab(self, tab: Tab) -> None:
         self.query("#tabs-list Tab.-active").remove_class("-active")
         tab.add_class("-active")
         self.active = tab.id or ""
-        self.query_one("#tabs-scroll").scroll_to_widget(tab, force=True)
+        self.query_one("#tabs-scroll").scroll_to_center(tab, force=True)
 
     def _on_underline_clicked(self, event: Underline.Clicked) -> None:
         """The underline was clicked.
@@ -471,7 +471,7 @@ def _scroll_active_tab(self) -> None:
         """Scroll the active tab into view."""
         if self.active_tab:
             try:
-                self.query_one("#tabs-scroll").scroll_to_widget(
+                self.query_one("#tabs-scroll").scroll_to_center(
                     self.active_tab, force=True
                 )
             except NoMatches:


Test patch:
diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py
--- a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py
+++ b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py
@@ -33,7 +33,7 @@ def compose(self) -> ComposeResult:
             yield Label(("SPAM\n" * 59)[:-1])
 
     def key_s(self) -> None:
-        self.query_one("#bullseye").scroll_to_center()
+        self.screen.scroll_to_center(self.query_one("#bullseye"))
 
 
 if __name__ == "__main__":
Repository: Textualize/textual

Issue:
Scrolling containers should be focusable by default
`ScrollHorizontal` and `ScrollVertical` should have `can_focus=True`.

Check this doesn't break any of the example apps.


Discussion:

Resolving patch:
diff --git a/src/textual/containers.py b/src/textual/containers.py
--- a/src/textual/containers.py
+++ b/src/textual/containers.py
@@ -31,7 +31,7 @@ class Vertical(Widget):
     """
 
 
-class VerticalScroll(Widget):
+class VerticalScroll(Widget, can_focus=True):
     """A container which arranges children vertically, with an automatic vertical scrollbar."""
 
     DEFAULT_CSS = """
@@ -55,7 +55,7 @@ class Horizontal(Widget):
     """
 
 
-class HorizontalScroll(Widget):
+class HorizontalScroll(Widget, can_focus=True):
     """A container which arranges children horizontally, with an automatic horizontal scrollbar."""
 
     DEFAULT_CSS = """


Test patch:
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -87,7 +87,7 @@ def test_input_and_focus(snap_compare):
 
 def test_buttons_render(snap_compare):
     # Testing button rendering. We press tab to focus the first button too.
-    assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"])
+    assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab", "tab"])
 
 
 def test_placeholder_render(snap_compare):
@@ -269,7 +269,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare):
 
 
 def test_borders_preview(snap_compare):
-    assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "enter"])
+    assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "tab", "enter"])
 
 
 def test_colors_preview(snap_compare):
@@ -325,7 +325,9 @@ def test_disabled_widgets(snap_compare):
 
 
 def test_focus_component_class(snap_compare):
-    assert snap_compare(SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab"])
+    assert snap_compare(
+        SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab", "tab"]
+    )
 
 
 def test_line_api_scrollbars(snap_compare):
Repository: Textualize/textual

Issue:
DataTable - When cursor_coordinate updates, we should scroll_cursor_visible
When the cursor moves out of the screen, we should scroll the cursor into visibility.

~~It seems like there is already code to do this, but because of #2458 the scrolling doesn't work.~~


Discussion:

Resolving patch:
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -950,6 +950,7 @@ def watch_cursor_coordinate(
             elif self.cursor_type == "column":
                 self.refresh_column(old_coordinate.column)
                 self._highlight_column(new_coordinate.column)
+            self._scroll_cursor_into_view()
 
     def _highlight_coordinate(self, coordinate: Coordinate) -> None:
         """Apply highlighting to the cell at the coordinate, and post event."""


Test patch:
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -991,3 +991,28 @@ def test_key_string_lookup():
     assert dictionary[RowKey("foo")] == "bar"
     assert dictionary["hello"] == "world"
     assert dictionary[RowKey("hello")] == "world"
+
+
+async def test_scrolling_cursor_into_view():
+    """Regression test for https://github.com/Textualize/textual/issues/2459"""
+
+    class TableApp(App):
+        CSS = "DataTable { height: 100%; }"
+
+        def compose(self):
+            yield DataTable()
+
+        def on_mount(self) -> None:
+            table = self.query_one(DataTable)
+            table.add_column("n")
+            table.add_rows([(n,) for n in range(300)])
+
+        def key_c(self):
+            self.query_one(DataTable).cursor_coordinate = Coordinate(200, 0)
+
+    app = TableApp()
+
+    async with app.run_test() as pilot:
+        await pilot.press("c")
+        await pilot.pause()
+        assert app.query_one(DataTable).scroll_y > 100
Repository: Textualize/textual

Issue:
OSError when the file system is not available
Textual (specifically App and DOMNode) assumes that the file containing source code for Widget classes is available and raises an `OSError` when this is not the case. This information is used to render the location of CSS styles.

I'm trying to get a Textual app running in an environment in which `getpath` doesn't work (Pyodide) because there's no file system in WebAssembly. Obviously, this is a pretty extreme environment, but I think the bug remains reasonable to address.

The calls to `getfile` are already wrapped in a try/except for `TypeError` with fallback behavior, so I propose that `OSError` be handled here, too. I *can* provide a standalone test case, though it's a little involved, so I'd rather just open a PR shortly, if you'll consider it.

<img width="574" alt="image" src="https://user-images.githubusercontent.com/156560/235913223-7ebd14eb-d9d2-4014-9215-9f60118e5263.png">


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
@@ -1660,7 +1660,7 @@ async def _process_messages(
                     app_css_path = (
                         f"{inspect.getfile(self.__class__)}:{self.__class__.__name__}"
                     )
-                except TypeError:
+                except (TypeError, OSError):
                     app_css_path = f"{self.__class__.__name__}"
                 self.stylesheet.add_source(
                     self.CSS, path=app_css_path, is_default_css=False
diff --git a/src/textual/dom.py b/src/textual/dom.py
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -418,7 +418,7 @@ def get_path(base: Type[DOMNode]) -> str:
             """Get a path to the DOM Node"""
             try:
                 return f"{getfile(base)}:{base.__name__}"
-            except TypeError:
+            except (TypeError, OSError):
                 return f"{base.__name__}"
 
         for tie_breaker, base in enumerate(self._node_bases):


Test patch:
Repository: Textualize/textual

Issue:
Tab events should have a `control` attribute
Some events, such as Tabs.TabActivated don't have a `control` attribute to allow them to use the new `@on` decorator.

We should check all events and add a `control` where required.


Discussion:
You beat me to it :)

Resolving patch:
diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py
--- a/src/textual/widgets/_tabs.py
+++ b/src/textual/widgets/_tabs.py
@@ -194,6 +194,15 @@ def __init__(self, tabs: Tabs, tab: Tab) -> None:
             self.tab = tab
             super().__init__()
 
+        @property
+        def control(self) -> Tabs:
+            """The tabs widget containing the tab that was activated.
+
+            This is an alias for [`TabActivated.tabs`][textual.widgets.Tabs.TabActivated.tabs]
+            which is used by the [`on`][textual.on] decorator.
+            """
+            return self.tabs
+
         def __rich_repr__(self) -> rich.repr.Result:
             yield self.tabs
             yield self.tab
@@ -213,6 +222,15 @@ def __init__(self, tabs: Tabs) -> None:
             self.tabs = tabs
             super().__init__()
 
+        @property
+        def control(self) -> Tabs:
+            """The tabs widget which was cleared.
+
+            This is an alias for [`Cleared.tabs`][textual.widgets.Tabs.Cleared] which
+            is used by the [`on`][textual.on] decorator.
+            """
+            return self.tabs
+
         def __rich_repr__(self) -> rich.repr.Result:
             yield self.tabs
 


Test patch:
Repository: Textualize/textual

Issue:
App title and sub_title should be converted to strings
I'm thinking that `App.title` and `App.sub_title` should be converted to strings.

Balancing convenience with hiding errors, this is probably the right thing to do 90% of the time.


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
@@ -316,6 +316,7 @@ def __init__(
         the name of the app if it doesn't.
 
         Assign a new value to this attribute to change the title.
+        The new value is always converted to string.
         """
 
         self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else ""
@@ -328,6 +329,7 @@ def __init__(
         the file being worker on.
 
         Assign a new value to this attribute to change the sub-title.
+        The new value is always converted to string.
         """
 
         self._logger = Logger(self._log)
@@ -406,6 +408,14 @@ def __init__(
         self.set_class(self.dark, "-dark-mode")
         self.set_class(not self.dark, "-light-mode")
 
+    def validate_title(self, title: Any) -> str:
+        """Make sure the title is set to a string."""
+        return str(title)
+
+    def validate_sub_title(self, sub_title: Any) -> str:
+        """Make sure the sub-title is set to a string."""
+        return str(sub_title)
+
     @property
     def workers(self) -> WorkerManager:
         """The [worker](guide/workers/) manager.


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -36,3 +36,33 @@ async def test_hover_update_styles():
         # We've hovered, so ensure the pseudoclass is present and background changed
         assert button.pseudo_classes == {"enabled", "hover"}
         assert button.styles.background != initial_background
+
+
+def test_setting_title():
+    app = MyApp()
+    app.title = None
+    assert app.title == "None"
+
+    app.title = ""
+    assert app.title == ""
+
+    app.title = 0.125
+    assert app.title == "0.125"
+
+    app.title = [True, False, 2]
+    assert app.title == "[True, False, 2]"
+
+
+def test_setting_sub_title():
+    app = MyApp()
+    app.sub_title = None
+    assert app.sub_title == "None"
+
+    app.sub_title = ""
+    assert app.sub_title == ""
+
+    app.sub_title = 0.125
+    assert app.sub_title == "0.125"
+
+    app.sub_title = [True, False, 2]
+    assert app.sub_title == "[True, False, 2]"
Repository: Textualize/textual

Issue:
Screen.dismiss should error if not the top-most screen
At the moment if you call `dismiss` on a screen that is *not* the top-most on the stack, strange things will happen.

I think the only solution here is for that to be an error.

i.e. if I push screens A, B, C on to the stack. Then I call `B.dismiss()` that should raise a `DismissError` (name TBD). Only `C.dismiss()` is valid, because it is a the top of the stack.


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
@@ -156,7 +156,7 @@ class ScreenError(Exception):
 
 
 class ScreenStackError(ScreenError):
-    """Raised when attempting to pop the last screen from the stack."""
+    """Raised when trying to manipulate the screen stack incorrectly."""
 
 
 class CssPathError(Exception):
diff --git a/src/textual/screen.py b/src/textual/screen.py
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -771,6 +771,10 @@ def dismiss(self, result: ScreenResultType | Type[_NoResult] = _NoResult) -> Non
         Args:
             result: The optional result to be passed to the result callback.
 
+        Raises:
+            ScreenStackError: If trying to dismiss a screen that is not at the top of
+                the stack.
+
         Note:
             If the screen was pushed with a callback, the callback will be
             called with the given result and then a call to
@@ -778,6 +782,12 @@ def dismiss(self, result: ScreenResultType | Type[_NoResult] = _NoResult) -> Non
             no callback was provided calling this method is the same as
             simply calling [`App.pop_screen`][textual.app.App.pop_screen].
         """
+        if self is not self.app.screen:
+            from .app import ScreenStackError
+
+            raise ScreenStackError(
+                f"Can't dismiss screen {self} that's not at the top of the stack."
+            )
         if result is not self._NoResult and self._result_callbacks:
             self._result_callbacks[-1](cast(ScreenResultType, result))
         self.app.pop_screen()


Test patch:
diff --git a/tests/test_screens.py b/tests/test_screens.py
--- a/tests/test_screens.py
+++ b/tests/test_screens.py
@@ -192,3 +192,17 @@ class MyApp(App[None]):
         assert app.focused is None
         app.pop_screen()
         assert app.focused.id == "two"
+
+
+async def test_dismiss_non_top_screen():
+    class MyApp(App[None]):
+        async def key_p(self) -> None:
+            self.bottom, top = Screen(), Screen()
+            await self.push_screen(self.bottom)
+            await self.push_screen(top)
+
+    app = MyApp()
+    async with app.run_test() as pilot:
+        await pilot.press("p")
+        with pytest.raises(ScreenStackError):
+            app.bottom.dismiss()
Repository: Textualize/textual

Issue:
Add a `description` parameter to the work decorator, to use in place of the auto-generated description.



Discussion:
Related issues: #2588

Resolving patch:
diff --git a/src/textual/_work_decorator.py b/src/textual/_work_decorator.py
--- a/src/textual/_work_decorator.py
+++ b/src/textual/_work_decorator.py
@@ -58,6 +58,7 @@ def work(
     group: str = "default",
     exit_on_error: bool = True,
     exclusive: bool = False,
+    description: str | None = None,
 ) -> Callable[FactoryParamSpec, Worker[ReturnType]] | Decorator:
     """A decorator used to create [workers](/guide/workers).
 
@@ -67,6 +68,9 @@ def work(
         group: A short string to identify a group of workers.
         exit_on_error: Exit the app if the worker raises an error. Set to `False` to suppress exceptions.
         exclusive: Cancel all workers in the same group.
+        description: Readable description of the worker for debugging purposes.
+            By default, it uses a string representation of the decorated method
+            and its arguments.
     """
 
     def decorator(
@@ -87,22 +91,25 @@ def decorated(
             self = args[0]
             assert isinstance(self, DOMNode)
 
-            try:
-                positional_arguments = ", ".join(repr(arg) for arg in args[1:])
-                keyword_arguments = ", ".join(
-                    f"{name}={value!r}" for name, value in kwargs.items()
-                )
-                tokens = [positional_arguments, keyword_arguments]
-                worker_description = f"{method.__name__}({', '.join(token for token in tokens if token)})"
-            except Exception:
-                worker_description = "<worker>"
+            if description is not None:
+                debug_description = description
+            else:
+                try:
+                    positional_arguments = ", ".join(repr(arg) for arg in args[1:])
+                    keyword_arguments = ", ".join(
+                        f"{name}={value!r}" for name, value in kwargs.items()
+                    )
+                    tokens = [positional_arguments, keyword_arguments]
+                    debug_description = f"{method.__name__}({', '.join(token for token in tokens if token)})"
+                except Exception:
+                    debug_description = "<worker>"
             worker = cast(
                 "Worker[ReturnType]",
                 self.run_worker(
                     partial(method, *args, **kwargs),
                     name=name or method.__name__,
                     group=group,
-                    description=worker_description,
+                    description=debug_description,
                     exclusive=exclusive,
                     exit_on_error=exit_on_error,
                 ),


Test patch: