Repository: Textualize/textual
Pull request: Add support for mounting widgets relative to other widgets
Addresses the requirements in #778.

In short, `App.mount`, `App.mount_all` and `Widget.mount` now accept either a `before` or `after` keyword argument, which can be an integer, a string selector or a widget, and the new widget(s) will be mounted relative to them.

A quick sandbox app for testing some of this would be:

```python
from textual.app import App, ComposeResult
from textual.widgets import Button, Header, Footer
from textual.reactive import reactive
from textual.binding import Binding

class Foundation(Button):
    DEFAULT_CSS = """
    Foundation {
        width: 100%;
        border: solid red;
        color: red;
        background: #303030;
        text-align: center;
        margin: 1;
        margin-left: 10;
        margin-right: 10;
    }
    """

class Addition(Foundation):
    DEFAULT_CSS = """
    Addition {
        border: dashed yellow;
        color: yellow;
    }
    """

class MountTest( App[ None ] ):

    before = reactive(True)

    counter = reactive(0)

    BINDINGS = [
        ("t", "tree", "Dump Tree"),
        ("w", "where", "Toggle before/after"),
        ("m", "mount_many", "Mount Many"),
        ("i", "mount_here", "Mount Here"),
        ("s", "mount_pos(0)", "Mount At Start"),
        ("e", "mount_pos(-1)", "Mount At End"),
        *[Binding(str(n), f"mount({n})", f"Before {n}", False) for n in range(10)],
    ]
    def compose(self) -> ComposeResult:
        yield Header()
        for n in range(10):
            yield Foundation(f"Foundation Widget {n}", id=f"foundation-{n}")
        yield Footer()

    def action_tree(self) -> None:
        self.log( self.tree )

    def action_where(self):
        self.before = not self.before

    def action_mount( self, n: int ) -> None:
        if self.before:
            self.screen.mount( Addition(f"Added {self.counter} BEFORE {n}"), before=f"#foundation-{n}")
        else:
            self.screen.mount( Addition(f"Added {self.counter} AFTER {n}"), after=f"#foundation-{n}")
        self.counter += 1

    def action_mount_many( self ) -> None:
        if self.before:
            self.mount_all(
                [ Addition( f"Many add {n} BEFORE 5") for n in range(3)],
                before=f"#foundation-5"
            )
        else:
            self.mount_all(
                [ Addition( f"Many add {n} AFTER 5") for n in range(3)],
                after=f"#foundation-5"
            )

    def action_mount_pos( self, pos: int ) -> None:
        if self.before:
            self.screen.mount( Addition(f"Added {self.counter} BEFORE INDEX {pos}"), before=pos)
        else:
            self.screen.mount( Addition(f"Added {self.counter} AFTER INDEX {pos}"), after=pos)
        self.counter += 1

    def action_mount_here( self ) -> None:
        if self.focused is not None:
            if self.before:
                self.screen.mount( Addition(f"Added {self.counter} BEFORE INDEX {self.focused!r}"), before=self.focused)
            else:
                self.screen.mount( Addition(f"Added {self.counter} AFTER INDEX {self.focused!r}"), after=self.focused)
            self.counter += 1

if __name__ == "__main__":
    MountTest().run()
```

**PLEASE NOTE**: This PR also contains a couple of small fixes to the `Pilot` as the unit tests for this PR found a couple of small issues.

Problem statement:
mount should have additional options to insert new widgets
Currently, mount() inserts new widgets at the end of the list.

We should provide some way of inserting widgets at the beginning and arbitrary point.

I'm thinking an `before` and `after` parameter which would accept an integer or a CSS query. And insert the new widget at a matching index or query. Indexes can be negative, so `after=-1` would insert at the end of the list.

The default for before and after should be None, which would insert at the end of the list.

Hints from issue discussion:
First thing to do, before tacking this, is to remove support for adding "anonymous" widgets vs "named" widgets. This will mean dropping support in the `App` and `Widget` `mount` methods, and also looking through the code, examples and documentation to remove any use and mention of this.
Having tidied up the parameters for the two `mount` methods (removing support for "named" widgets), the next thing to do is to look at [`NodeList`](https://github.com/Textualize/textual/blob/93e75f3f0e84b86883c253a30e4a4ec1b890ff1f/src/textual/_node_list.py`) with a view to adding other methods of getting a widget into the list, that isn't just a simple append.
From what I can see there are no unit tests for `NodeList` -- this might be the perfect moment to start to add some.
Reviewing the higher-level requirement, and thinking about how it could be used, I'd like to just recheck the approach suggested. As I understand it, right now, we have `mount`, which (if we can forgive relating this to web development for a moment) would seem to be equivalent to [`appendChild`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild). If that's the case, I can't help but feel that an analogous interface (which I suspect folk would be looking for), would be something akin to [`insertBefore`](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) (although of course we may want both before and after versions). What I'm not sure I'm getting is when someone would want to work with numeric indexes.

Taking our stopwatch tutorial, with this in mind:

```python
def action_add_stopwatch(self) -> None:
    """An action to add a timer."""
    new_stopwatch = Stopwatch()
    self.query_one("#timers").mount(new_stopwatch)
    new_stopwatch.scroll_visible()
```

I can imagine wanting to turn this into "add after the focused stopwatch or at the end if one isn't focused", with some query to find the relevant `Stopwatch` within the `#timers` container resulting in something akin to:

```python
self.query_one("#timers").mount_after(focused_stopwatch, new_stopwatch)
```

or if one isn't focused, the original code happening.

Likewise, an "insert at the focused position" resulting in something like:

```python
self.query_one("#timers").mount_before(focused_stopwatch, new_stopwatch)
```

What I can't quite see is when someone would want to do this sort of thing with a numeric index, and what the code would look like. Even if someone wanted to use an index with the stopwatches, would it make more sense to do something like:

```python
self.query_one("#timers").mount_before(self.query(Stopwatch)[3], new_stopwatch)
```

It could be that I'm not seeing what the use case is for the initial suggested approach, in which case I think an example or two to illustrate how that would work would be helpful.

Commit message: Add a reminder to the mount tests about query_one

See https://github.com/Textualize/textual/issues/1096

Patch to review:
diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py
index fa5570fc48..a2a2d1d783 100644
--- a/src/textual/_node_list.py
+++ b/src/textual/_node_list.py
@@ -39,6 +39,20 @@ def __len__(self) -> int:
     def __contains__(self, widget: Widget) -> bool:
         return widget in self._nodes
 
+    def _index(self, widget: Widget) -> int:
+        """Return the index of the given widget.
+
+        Args:
+            widget (Widget): The widget to find in the node list.
+
+        Returns:
+            int: The index of the widget in the node list.
+
+        Raises:
+            ValueError: If the widget is not in the node list.
+        """
+        return self._nodes.index(widget)
+
     def _append(self, widget: Widget) -> None:
         """Append a Widget.
 
@@ -50,6 +64,17 @@ def _append(self, widget: Widget) -> None:
             self._nodes_set.add(widget)
             self._updates += 1
 
+    def _insert(self, index: int, widget: Widget) -> None:
+        """Insert a Widget.
+
+        Args:
+            widget (Widget): A widget.
+        """
+        if widget not in self._nodes_set:
+            self._nodes.insert(index, widget)
+            self._nodes_set.add(widget)
+            self._updates += 1
+
     def _remove(self, widget: Widget) -> None:
         """Remove a widget from the list.
 
diff --git a/src/textual/app.py b/src/textual/app.py
index 7ea329ed45..9659b399f6 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -675,12 +675,16 @@ async def run_app(app) -> None:
         # Wait until the app has performed all startup routines.
         await app_ready_event.wait()
 
-        # Context manager returns pilot object to manipulate the app
-        yield Pilot(app)
+        # Get the app in an active state.
+        app._set_active()
 
-        # Shutdown the app cleanly
-        await app._shutdown()
-        await app_task
+        # Context manager returns pilot object to manipulate the app
+        try:
+            yield Pilot(app)
+        finally:
+            # Shutdown the app cleanly
+            await app._shutdown()
+            await app_task
 
     async def run_async(
         self,
@@ -822,27 +826,55 @@ def update_styles(self, node: DOMNode | None = None) -> None:
         self._require_stylesheet_update.add(self.screen if node is None else node)
         self.check_idle()
 
-    def mount(self, *widgets: Widget) -> AwaitMount:
-        """Mount the given widgets.
+    def mount(
+        self,
+        *widgets: Widget,
+        before: int | str | Widget | None = None,
+        after: int | str | Widget | None = None,
+    ) -> AwaitMount:
+        """Mount the given widgets relative to the app's screen.
 
         Args:
             *widgets (Widget): The widget(s) to mount.
+            before (int | str | Widget, optional): Optional location to mount before.
+            after (int | str | Widget, optional): Optional location to mount after.
 
         Returns:
             AwaitMount: An awaitable object that waits for widgets to be mounted.
+
+        Raises:
+            MountError: If there is a problem with the mount request.
+
+        Note:
+            Only one of ``before`` or ``after`` can be provided. If both are
+            provided a ``MountError`` will be raised.
         """
-        return AwaitMount(self._register(self.screen, *widgets))
+        return self.screen.mount(*widgets, before=before, after=after)
 
-    def mount_all(self, widgets: Iterable[Widget]) -> AwaitMount:
+    def mount_all(
+        self,
+        widgets: Iterable[Widget],
+        before: int | str | Widget | None = None,
+        after: int | str | Widget | None = None,
+    ) -> AwaitMount:
         """Mount widgets from an iterable.
 
         Args:
             widgets (Iterable[Widget]): An iterable of widgets.
+            before (int | str | Widget, optional): Optional location to mount before.
+            after (int | str | Widget, optional): Optional location to mount after.
 
         Returns:
             AwaitMount: An awaitable object that waits for widgets to be mounted.
+
+        Raises:
+            MountError: If there is a problem with the mount request.
+
+        Note:
+            Only one of ``before`` or ``after`` can be provided. If both are
+            provided a ``MountError`` will be raised.
         """
-        return self.mount(*widgets)
+        return self.mount(*widgets, before=before, after=after)
 
     def is_screen_installed(self, screen: Screen | str) -> bool:
         """Check if a given screen has been installed.
@@ -1290,23 +1322,64 @@ def _on_idle(self) -> None:
             self._require_stylesheet_update.clear()
             self.stylesheet.update_nodes(nodes, animate=True)
 
-    def _register_child(self, parent: DOMNode, child: Widget) -> bool:
+    def _register_child(
+        self, parent: DOMNode, child: Widget, before: int | None, after: int | None
+    ) -> bool:
+
+        # Let's be 100% sure that we've not been asked to do a before and an
+        # after at the same time. It's possible that we can remove this
+        # check later on, but for the purposes of development right now,
+        # it's likely a good idea to keep it here to check assumptions in
+        # the rest of the code.
+        if before is not None and after is not None:
+            raise AppError(
+                "A child can only be registered before or after, not before and after"
+            )
+
+        # If we don't already know about this widget...
         if child not in self._registry:
-            parent.children._append(child)
+
+            # Now to figure out where to place it. If we've got a `before`...
+            if before is not None:
+                # ...it's safe to NodeList._insert before that location.
+                parent.children._insert(before, child)
+            elif after is not None and after != -1:
+                # In this case we've got an after. -1 holds the special
+                # position (for now) of meaning "okay really what I mean is
+                # do an append, like if I'd asked to add with no before or
+                # after". So... we insert before the next item in the node
+                # list, iff after isn't -1.
+                parent.children._insert(after + 1, child)
+            else:
+                # At this point we appear to not be adding before or after,
+                # or we've got a before/after value that really means
+                # "please append". So...
+                parent.children._append(child)
+
+            # Now that the widget is in the NodeList of its parent, sort out
+            # the rest of the admin.
             self._registry.add(child)
             child._attach(parent)
             child._post_register(self)
             child._start_messages()
             return True
+
         return False
 
-    def _register(self, parent: DOMNode, *widgets: Widget) -> list[Widget]:
+    def _register(
+        self,
+        parent: DOMNode,
+        *widgets: Widget,
+        before: int | None = None,
+        after: int | None = None,
+    ) -> list[Widget]:
         """Register widget(s) so they may receive events.
 
         Args:
             parent (DOMNode): Parent node.
             *widgets: The widget(s) to register.
-
+            before (int, optional): A location to mount before.
+            after (int, option): A location to mount after.
         Returns:
             list[Widget]: List of modified widgets.
 
@@ -1315,13 +1388,19 @@ def _register(self, parent: DOMNode, *widgets: Widget) -> list[Widget]:
         if not widgets:
             return []
 
-        apply_stylesheet = self.stylesheet.apply
+        new_widgets = list(widgets)
+        if before is not None or after is not None:
+            # There's a before or after, which means there's going to be an
+            # insertion, so make it easier to get the new things in the
+            # correct order.
+            new_widgets = reversed(new_widgets)
 
-        for widget in widgets:
+        apply_stylesheet = self.stylesheet.apply
+        for widget in new_widgets:
             if not isinstance(widget, Widget):
                 raise AppError(f"Can't register {widget!r}; expected a Widget instance")
             if widget not in self._registry:
-                self._register_child(parent, widget)
+                self._register_child(parent, widget, before, after)
                 if widget.children:
                     self._register(widget, *widget.children)
                 apply_stylesheet(widget)
diff --git a/src/textual/dom.py b/src/textual/dom.py
index aa8f9a1b49..bcd3ab6832 100644
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -13,6 +13,7 @@
     TypeVar,
     cast,
     overload,
+    Union,
 )
 
 import rich.repr
diff --git a/src/textual/widget.py b/src/textual/widget.py
index 9d1f50bb27..dab6865856 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -375,16 +375,103 @@ def _get_virtual_dom(self) -> Iterable[Widget]:
         if self._scrollbar_corner is not None:
             yield self._scrollbar_corner
 
-    def mount(self, *widgets: Widget) -> AwaitMount:
-        """Mount child widgets (making this widget a container).
+    class MountError(Exception):
+        """Error raised when there was a problem with the mount request."""
+
+    def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]:
+        """Attempt to locate the point where the caller wants to mount something.
+
+        Args:
+            spot (int | str | Widget): The spot to find.
+
+        Returns:
+            tuple[Widget, int]: The parent and the location in its child list.
+
+        Raises:
+            Widget.MountError: If there was an error finding where to mount a widget.
+
+        The rules of this method are:
+
+        - Given an ``int``, parent is ``self`` and location is the integer value.
+        - Given a ``Widget``, parent is the widget's parent and location is
+          where the widget is found in the parent's ``children``. If it
+          can't be found a ``MountError`` will be raised.
+        - Given a string, it is used to perform a ``query_one`` and then the
+          result is used as if a ``Widget`` had been given.
+        """
+
+        # A numeric location means at that point in our child list.
+        if isinstance(spot, int):
+            return self, spot
+
+        # If we've got a string, that should be treated like a query that
+        # can be passed to query_one. So let's use that to get a widget to
+        # work on.
+        if isinstance(spot, str):
+            spot = self.query_one(spot, Widget)
+
+        # At this point we should have a widget, either because we got given
+        # one, or because we pulled one out of the query. First off, does it
+        # have a parent? There's no way we can use it as a sibling to make
+        # mounting decisions if it doesn't have a parent.
+        if spot.parent is None:
+            raise self.MountError(
+                f"Unable to find relative location of {spot!r} because it has no parent"
+            )
+
+        # We've got a widget. It has a parent. It has (zero or more)
+        # children. We should be able to go looking for the widget's
+        # location amongst its parent's children.
+        try:
+            return spot.parent, spot.parent.children._index(spot)
+        except ValueError:
+            raise self.MountError(f"{spot!r} is not a child of {self!r}") from None
+
+    def mount(
+        self,
+        *widgets: Widget,
+        before: int | str | Widget | None = None,
+        after: int | str | Widget | None = None,
+    ) -> AwaitMount:
+        """Mount widgets below this widget (making this widget a container).
 
         Args:
             *widgets (Widget): The widget(s) to mount.
+            before (int | str | Widget, optional): Optional location to mount before.
+            after (int | str | Widget, optional): Optional location to mount after.
 
         Returns:
             AwaitMount: An awaitable object that waits for widgets to be mounted.
+
+        Raises:
+            MountError: If there is a problem with the mount request.
+
+        Note:
+            Only one of ``before`` or ``after`` can be provided. If both are
+            provided a ``MountError`` will be raised.
         """
-        return AwaitMount(self.app._register(self, *widgets))
+
+        # Saying you want to mount before *and* after something is an error.
+        if before is not None and after is not None:
+            raise self.MountError(
+                "Only one of `before` or `after` can be handled -- not both"
+            )
+
+        # Decide the final resting place depending on what we've been asked
+        # to do.
+        if before is not None:
+            parent, before = self._find_mount_point(before)
+            self.log.debug(f"MOUNT under {parent!r} before {before!r} ")
+        elif after is not None:
+            parent, after = self._find_mount_point(after)
+            self.log.debug(f"MOUNT under {parent!r} after {after!r} ")
+        else:
+            parent = self
+            self.log.debug(f"MOUNT under {self!r} at the end of the child list")
+
+        return AwaitMount(
+            self.app._register(parent, *widgets, before=before, after=after)
+        )
 
     def compose(self) -> ComposeResult:
         """Called by Textual to create child widgets.
diff --git a/tests/test_node_list.py b/tests/test_node_list.py
index 93093bb898..fec53dbb92 100644
--- a/tests/test_node_list.py
+++ b/tests/test_node_list.py
@@ -1,3 +1,5 @@
+import pytest
+
 from textual.widget import Widget
 from textual._node_list import NodeList
 
@@ -19,6 +21,16 @@ def test_repeat_add_one():
         nodes._append(widget)
     assert len(nodes)==1
 
+def test_insert():
+    nodes = NodeList()
+    widget1 = Widget()
+    widget2 = Widget()
+    widget3 = Widget()
+    nodes._append(widget1)
+    nodes._append(widget3)
+    nodes._insert(1,widget2)
+    assert list(nodes) == [widget1,widget2,widget3]
+
 def test_truthy():
     """Does a node list act as a truthy object?"""
     nodes = NodeList()
@@ -35,6 +47,15 @@ def test_contains():
     assert widget in nodes
     assert Widget() not in nodes
 
+def test_index():
+    """Can we get the index of a widget in the list?"""
+    widget = Widget()
+    nodes = NodeList()
+    with pytest.raises(ValueError):
+        _ = nodes._index(widget)
+    nodes._append(widget)
+    assert nodes._index(widget) == 0
+
 def test_remove():
     """Can we remove a widget we've added?"""
     widget = Widget()
diff --git a/tests/test_widget_mount_point.py b/tests/test_widget_mount_point.py
new file mode 100644
index 0000000000..bcdc41dde6
--- /dev/null
+++ b/tests/test_widget_mount_point.py
@@ -0,0 +1,40 @@
+import pytest
+
+from textual.widget import Widget
+
+
+class Content(Widget):
+    pass
+
+
+class Body(Widget):
+    pass
+
+
+def test_find_dom_spot():
+
+    # Build up a "fake" DOM for an application.
+    screen = Widget(name="Screen")
+    header = Widget(name="Header", id="header")
+    body = Body(id="body")
+    content = [Content(id=f"item{n}") for n in range(1000)]
+    body._add_children(*content)
+    footer = Widget(name="Footer", id="footer")
+    screen._add_children(header, body, footer)
+
+    # Just as a quick double-check, make sure the main components are in
+    # their intended place.
+    assert list(screen.children) == [header, body, footer]
+
+    # Now check that we find what we're looking for in the places we expect
+    # to find them.
+    assert screen._find_mount_point(1) == (screen, 1)
+    assert screen._find_mount_point(body) == screen._find_mount_point(1)
+    assert screen._find_mount_point("Body") == screen._find_mount_point(body)
+    assert screen._find_mount_point("#body") == screen._find_mount_point(1)
+
+    # Finally, let's be sure that we get an error if, for some odd reason,
+    # we go looking for a widget that isn't actually part of the DOM we're
+    # looking in.
+    with pytest.raises(Widget.MountError):
+        _ = screen._find_mount_point(Widget())
diff --git a/tests/test_widget_mounting.py b/tests/test_widget_mounting.py
new file mode 100644
index 0000000000..7b3d4769a8
--- /dev/null
+++ b/tests/test_widget_mounting.py
@@ -0,0 +1,99 @@
+import pytest
+
+from textual.app import App
+from textual.widgets import Static
+
+async def test_mount_via_app() -> None:
+    """Perform mount tests via the app."""
+
+    # Make a background set of widgets.
+    widgets = [Static(id=f"starter-{n}") for n in range( 10 )]
+
+    async with App().run_test() as pilot:
+        # Mount the first one and make sure it's there.
+        await pilot.app.mount(widgets[0])
+        assert len(pilot.app.screen.children) == 1
+        assert pilot.app.screen.children[0] == widgets[0]
+
+        # Mount the next 2 widgets via mount.
+        await pilot.app.mount(*widgets[1:3])
+        assert list(pilot.app.screen.children) == widgets[0:3]
+
+        # Finally mount the rest of the widgets via mount_all.
+        await pilot.app.mount_all(widgets[3:])
+        assert list(pilot.app.screen.children) == widgets
+
+    async with App().run_test() as pilot:
+        # Mount a widget before -1, which is "before the end".
+        penultimate = Static(id="penultimate")
+        await pilot.app.mount_all(widgets)
+        await pilot.app.mount(penultimate, before=-1)
+        assert pilot.app.screen.children[-2] == penultimate
+
+    async with App().run_test() as pilot:
+        # Mount a widget after -1, which is "at the end".
+        ultimate = Static(id="ultimate")
+        await pilot.app.mount_all(widgets)
+        await pilot.app.mount(ultimate, after=-1)
+        assert pilot.app.screen.children[-1] == ultimate
+
+    async with App().run_test() as pilot:
+        # Mount a widget before 0, which is "at the start".
+        start = Static(id="start")
+        await pilot.app.mount_all(widgets)
+        await pilot.app.mount(start, before=0)
+        assert pilot.app.screen.children[0] == start
+
+    async with App().run_test() as pilot:
+        # Mount a widget after 0. You get the idea...
+        second = Static(id="second")
+        await pilot.app.mount_all(widgets)
+        await pilot.app.mount(second, after=0)
+        assert pilot.app.screen.children[1] == second
+
+    async with App().run_test() as pilot:
+        # Mount a widget relative to another via query.
+        queue_jumper = Static(id="queue-jumper")
+        await pilot.app.mount_all(widgets)
+        await pilot.app.mount(queue_jumper, after="#starter-5")
+        assert pilot.app.screen.children[6] == queue_jumper
+
+    async with App().run_test() as pilot:
+        # Mount a widget relative to another via query.
+        queue_jumper = Static(id="queue-jumper")
+        await pilot.app.mount_all(widgets)
+        await pilot.app.mount(queue_jumper, after=widgets[5])
+        assert pilot.app.screen.children[6] == queue_jumper
+
+    async with App().run_test() as pilot:
+        # Make sure we get told off for trying to before and after.
+        await pilot.app.mount_all(widgets)
+        with pytest.raises(Static.MountError):
+            await pilot.app.mount(Static(), before=2, after=2)
+
+    async with App().run_test() as pilot:
+        # Make sure we get told off trying to mount relative to something
+        # that isn't actually in the DOM.
+        await pilot.app.mount_all(widgets)
+        with pytest.raises(Static.MountError):
+            await pilot.app.mount(Static(), before=Static())
+        with pytest.raises(Static.MountError):
+            await pilot.app.mount(Static(), after=Static())
+
+    # TODO: At the moment query_one() simply takes a query and returns the
+    # .first() item. As such doing a query_one() that gets more than one
+    # thing isn't an error, it just skims off the first thing. OTOH the
+    # intention of before= and after= with a selector is that an exception
+    # will be thrown -- the exception being the own that should be thrown
+    # from query_one(). So, this test here is a TODO test because we'll wait
+    # for a change to query_one() and then its exception will just bubble
+    # up.
+    #
+    # See https://github.com/Textualize/textual/issues/1096
+    #
+    # async with App().run_test() as pilot:
+    #     # Make sure we get an error if we try and mount with a selector that
+    #     # results in more than one hit.
+    #     await pilot.app.mount_all(widgets)
+    #     with pytest.raises( ?Something? ):
+    #         await pilot.app.mount(Static(), before="Static")


Code review comments:
diff_hunk: @@ -13,6 +13,7 @@
     TypeVar,
     cast,
     overload,
+    Union,
path: src/textual/dom.py
text: @user1:
Is this used?

@author:
Good catch, nope. Did a lot of changes in there but backed them out in the end so that'll be a hangover.

diff_hunk: @@ -39,6 +39,20 @@ def __len__(self) -> int:
     def __contains__(self, widget: Widget) -> bool:
         return widget in self._nodes
 
+    def _index(self, widget: Widget) -> int:
path: src/textual/_node_list.py
text: @user1:
I'd be tempted to make this public. Since this object is already list-like.

@author:
Will do. For anyone reading: there was a followup chat where we decided that we *do* want the read-only list-a-like methods exposed so they provide some utility when someone does things with `.children`; it's just the methods that modify the `NodeList` that should be treated as private.

@author:
Done.