Merge branch 'main' into promote-disabled

This commit is contained in:
Dave Pearson
2023-02-14 10:31:36 +00:00
18 changed files with 150 additions and 103 deletions

View File

@@ -19,13 +19,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750
- Added `DOMNode.watch` and `DOMNode.is_attached` methods https://github.com/Textualize/textual/pull/1750
- Added `DOMNode.css_tree` which is a renderable that shows the DOM and CSS https://github.com/Textualize/textual/pull/1778
- Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778
### Changed
- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637
- `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471
- Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746
- `App.install_screen` name is no longer optional https://github.com/Textualize/textual/pull/1778
- `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778
- `DOMNode.tree` now displays simple DOM structure only https://github.com/Textualize/textual/pull/1778
- `App.install_screen` now returns None rather than AwaitMount https://github.com/Textualize/textual/pull/1778
- `DOMNode.children` is now a simple sequence, the NodesList is exposed as `DOMNode._nodes` https://github.com/Textualize/textual/pull/1778
### Fixed

View File

@@ -15,7 +15,7 @@ I'm taking a brief break from blogging about [Textual](https://github.com/Textua
If you have ever used `asyncio.create_task` you may have created a bug for yourself that is challenging (read *almost impossible*) to reproduce. If it occurs, your code will likely fail in unpredictable ways.
The root cause of this [Heisenbug](https://en.wikipedia.org/wiki/Heisenbug) is that if you don't hold a reference to the task object returned by `create_task` then the task may disappear without warning when Python runs garbage collection. In other words the code in your task will stop running with no obvious indication why.
The root cause of this [Heisenbug](https://en.wikipedia.org/wiki/Heisenbug) is that if you don't hold a reference to the task object returned by `create_task` then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why.
This behavior is [well documented](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task), as you can see from this excerpt (emphasis mine):

View File

@@ -21,7 +21,7 @@ The example below creates a simple tree.
--8<-- "docs/examples/widgets/tree.py"
```
Tree widgets have a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree,TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.
Tree widgets have a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree.TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.
## Reactive Attributes

View File

@@ -47,7 +47,6 @@ typing-extensions = "^4.0.0"
aiohttp = { version = ">=3.8.1", optional = true }
click = {version = ">=8.1.2", optional = true}
msgpack = { version = ">=1.0.3", optional = true }
nanoid = ">=2.0.0"
mkdocs-exclude = "^1.0.2"
[tool.poetry.extras]

View File

@@ -57,7 +57,7 @@ class Layout(ABC):
Returns:
Width of the content.
"""
if not widget.children:
if not widget._nodes:
width = 0
else:
# Use a size of 0, 0 to ignore relative sizes, since those are flexible anyway
@@ -85,7 +85,7 @@ class Layout(ABC):
Returns:
Content height (in lines).
"""
if not widget.children:
if not widget._nodes:
height = 0
else:
# Use a height of zero to ignore relative heights

View File

@@ -25,15 +25,15 @@ from typing import (
Generic,
Iterable,
List,
Sequence,
Type,
TypeVar,
Union,
cast,
overload,
)
from weakref import WeakSet, WeakValueDictionary
from weakref import WeakSet
import nanoid
import rich
import rich.repr
from rich.console import Console, RenderableType
@@ -385,9 +385,7 @@ class App(Generic[ReturnType], DOMNode):
self.scroll_sensitivity_y: float = 2.0
"""Number of lines to scroll in the Y direction with wheel or trackpad."""
self._installed_screens: WeakValueDictionary[
str, Screen | Callable[[], Screen]
] = WeakValueDictionary()
self._installed_screens: dict[str, Screen | Callable[[], Screen]] = {}
self._installed_screens.update(**self.SCREENS)
self.devtools: DevtoolsClient | None = None
@@ -420,6 +418,14 @@ class App(Generic[ReturnType], DOMNode):
"""ReturnType | None: The return type of the app."""
return self._return_value
@property
def children(self) -> Sequence["Widget"]:
"""A view on to the children which contains just the screen."""
try:
return (self.screen,)
except ScreenError:
return ()
def animate(
self,
attribute: str,
@@ -1265,15 +1271,16 @@ class App(Generic[ReturnType], DOMNode):
return await_mount
return AwaitMount(self.screen, [])
def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount:
def install_screen(self, screen: Screen, name: str) -> None:
"""Install a screen.
Installing a screen prevents Textual from destroying it when it is no longer on the screen stack.
Note that you don't need to install a screen to use it. See [push_screen][textual.app.App.push_screen]
or [switch_screen][textual.app.App.switch_screen] to make a new screen current.
Args:
screen: Screen to install.
name: Unique name of screen or None to auto-generate.
Defaults to None.
name: Unique name to identify the screen.
Raises:
ScreenError: If the screen can't be installed.
@@ -1281,8 +1288,6 @@ class App(Generic[ReturnType], DOMNode):
Returns:
An awaitable that awaits the mounting of the screen and its children.
"""
if name is None:
name = nanoid.generate()
if name in self._installed_screens:
raise ScreenError(f"Can't install screen; {name!r} is already installed")
if screen in self._installed_screens.values():
@@ -1290,13 +1295,17 @@ class App(Generic[ReturnType], DOMNode):
"Can't install screen; {screen!r} has already been installed"
)
self._installed_screens[name] = screen
_screen, await_mount = self._get_screen(name) # Ensures screen is running
self.log.system(f"{screen} INSTALLED name={name!r}")
return await_mount
def uninstall_screen(self, screen: Screen | str) -> str | None:
"""Uninstall a screen. If the screen was not previously installed then this
method is a null-op.
"""Uninstall a screen.
If the screen was not previously installed then this method is a null-op.
Uninstalling a screen allows Textual to delete it when it is popped or switched.
Note that uninstalling a screen is only required if you have previously installed it
with [install_screen][textual.app.App.install_screen].
Textual will also uninstall screens automatically on exit.
Args:
screen: The screen to uninstall or the name of a installed screen.
@@ -1641,19 +1650,19 @@ class App(Generic[ReturnType], DOMNode):
# 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)
parent._nodes._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)
parent._nodes._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)
parent._nodes._append(child)
# Now that the widget is in the NodeList of its parent, sort out
# the rest of the admin.
@@ -1698,8 +1707,8 @@ class App(Generic[ReturnType], DOMNode):
raise AppError(f"Can't register {widget!r}; expected a Widget instance")
if widget not in self._registry:
self._register_child(parent, widget, before, after)
if widget.children:
self._register(widget, *widget.children)
if widget._nodes:
self._register(widget, *widget._nodes)
apply_stylesheet(widget)
if not self._running:
@@ -1716,7 +1725,7 @@ class App(Generic[ReturnType], DOMNode):
"""
widget.reset_focus()
if isinstance(widget._parent, Widget):
widget._parent.children._remove(widget)
widget._parent._nodes._remove(widget)
widget._detach()
self._registry.discard(widget)
@@ -1894,6 +1903,7 @@ class App(Generic[ReturnType], DOMNode):
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Compose):
self.log(event)
screen = Screen(id="_default")
self._register(self, screen)
self._screen_stack.append(screen)
@@ -2098,7 +2108,7 @@ class App(Generic[ReturnType], DOMNode):
# snipping each affected branch from the DOM.
for widget in pruned_remove:
if widget.parent is not None:
widget.parent.children._remove(widget)
widget.parent._nodes._remove(widget)
# Return the list of widgets that should end up being sent off in a
# prune event.
@@ -2117,10 +2127,10 @@ class App(Generic[ReturnType], DOMNode):
while stack:
widget = pop()
children = [*widget.children, *widget._get_virtual_dom()]
children = [*widget._nodes, *widget._get_virtual_dom()]
if children:
yield children
for child in widget.children:
for child in widget._nodes:
push(child)
def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove:

View File

@@ -7,6 +7,7 @@ from typing import (
ClassVar,
Iterable,
Iterator,
Sequence,
Type,
TypeVar,
cast,
@@ -132,7 +133,7 @@ class DOMNode(MessagePump):
check_identifiers("class name", *_classes)
self._classes.update(_classes)
self.children: NodeList = NodeList()
self._nodes: NodeList = NodeList()
self._css_styles: Styles = Styles(self)
self._inline_styles: Styles = Styles(self)
self.styles: RenderStyles = RenderStyles(
@@ -150,6 +151,11 @@ class DOMNode(MessagePump):
super().__init__()
@property
def children(self) -> Sequence["Widget"]:
"""A view on to the children."""
return self._nodes
@property
def auto_refresh(self) -> float | None:
return self._auto_refresh
@@ -484,7 +490,7 @@ class DOMNode(MessagePump):
return self.styles.visibility != "hidden"
@visible.setter
def visible(self, new_value: bool) -> None:
def visible(self, new_value: bool | str) -> None:
if isinstance(new_value, bool):
self.styles.visibility = "visible" if new_value else "hidden"
elif new_value in VALID_VISIBILITY:
@@ -497,10 +503,27 @@ class DOMNode(MessagePump):
@property
def tree(self) -> Tree:
"""Get a Rich tree object which will recursively render the structure of the node tree.
"""Get a Rich tree object which will recursively render the structure of the node tree."""
Returns:
A Rich object which may be printed.
def render_info(node: DOMNode) -> Pretty:
return Pretty(node)
tree = Tree(render_info(self))
def add_children(tree, node):
for child in node.children:
info = render_info(child)
branch = tree.add(info)
if tree.children:
add_children(branch, child)
add_children(tree, self)
return tree
@property
def css_tree(self) -> Tree:
"""Get a Rich tree object which will recursively render the structure of the node tree,
which also displays CSS and size information.
"""
from rich.columns import Columns
from rich.console import Group
@@ -648,7 +671,7 @@ class DOMNode(MessagePump):
Children of this widget which will be displayed.
"""
return [child for child in self.children if child.display]
return [child for child in self._nodes if child.display]
def watch(
self,
@@ -691,7 +714,7 @@ class DOMNode(MessagePump):
Args:
node: A DOM node.
"""
self.children._append(node)
self._nodes._append(node)
node._attach(self)
def _add_children(self, *nodes: Widget) -> None:
@@ -700,7 +723,7 @@ class DOMNode(MessagePump):
Args:
*nodes: Positional args should be new DOM nodes.
"""
_append = self.children._append
_append = self._nodes._append
for node in nodes:
node._attach(self)
_append(node)

View File

@@ -236,5 +236,4 @@ class LinuxDriver(Driver):
except Exception as error:
log(error)
finally:
with timer("selector.close"):
selector.close()
selector.close()

View File

@@ -53,7 +53,7 @@ def walk_depth_first(
"""
from textual.dom import DOMNode
stack: list[Iterator[DOMNode]] = [iter(root.children)]
stack: list[Iterator[DOMNode]] = [iter(root._nodes)]
pop = stack.pop
push = stack.append
check_type = filter_type or DOMNode

View File

@@ -303,7 +303,7 @@ class Widget(DOMNode):
"""
parent = self.parent
if parent is not None:
siblings = list(parent.children)
siblings = list(parent._nodes)
siblings.remove(self)
return siblings
else:
@@ -394,7 +394,7 @@ class Widget(DOMNode):
NoMatches: if no children could be found for this ID
WrongType: if the wrong type was found.
"""
child = self.children._get_by_id(id)
child = self._nodes._get_by_id(id)
if child is None:
raise NoMatches(f"No child found with id={id!r}")
if expect_type is None:
@@ -476,7 +476,7 @@ class Widget(DOMNode):
"""
assert self.is_container
cache_key = (size, self.children._updates)
cache_key = (size, self._nodes._updates)
if (
self._arrangement_cache_key == cache_key
and self._cached_arrangement is not None
@@ -485,7 +485,7 @@ class Widget(DOMNode):
self._arrangement_cache_key = cache_key
arrangement = self._cached_arrangement = arrange(
self, self.children, size, self.screen.size
self, self._nodes, size, self.screen.size
)
return arrangement
@@ -553,7 +553,7 @@ class Widget(DOMNode):
# children. We should be able to go looking for the widget's
# location amongst its parent's children.
try:
return cast("Widget", spot.parent), spot.parent.children.index(spot)
return cast("Widget", spot.parent), spot.parent._nodes.index(spot)
except ValueError:
raise MountError(f"{spot!r} is not a child of {self!r}") from None
@@ -691,7 +691,7 @@ class Widget(DOMNode):
"""Ensure a given child reference is a Widget."""
if isinstance(child, int):
try:
child = self.children[child]
child = self._nodes[child]
except IndexError:
raise WidgetError(
f"An index of {child} for the child to {called} is out of bounds"
@@ -700,7 +700,7 @@ class Widget(DOMNode):
# We got an actual widget, so let's be sure it really is one of
# our children.
try:
_ = self.children.index(child)
_ = self._nodes.index(child)
except ValueError:
raise WidgetError(f"{child!r} is not a child of {self!r}") from None
return child
@@ -713,11 +713,11 @@ class Widget(DOMNode):
# child; where we're moving it to, which should be within the child
# list; and how we're supposed to move it. All that's left is doing
# the right thing.
self.children._remove(child)
self._nodes._remove(child)
if before is not None:
self.children._insert(self.children.index(target), child)
self._nodes._insert(self._nodes.index(target), child)
else:
self.children._insert(self.children.index(target) + 1, child)
self._nodes._insert(self._nodes.index(target) + 1, child)
# Request a refresh.
self.refresh(layout=True)
@@ -1199,9 +1199,7 @@ class Widget(DOMNode):
List of widgets that can receive focus.
"""
focusable = [
child for child in self.children if child.display and child.visible
]
focusable = [child for child in self._nodes if child.display and child.visible]
return sorted(focusable, key=attrgetter("_focus_sort_key"))
@property
@@ -1295,7 +1293,7 @@ class Widget(DOMNode):
Returns:
True if this widget is a container.
"""
return self.styles.layout is not None or bool(self.children)
return self.styles.layout is not None or bool(self._nodes)
@property
def is_scrollable(self) -> bool:
@@ -1304,7 +1302,7 @@ class Widget(DOMNode):
Returns:
True if this widget may be scrolled.
"""
return self.styles.layout is not None or bool(self.children)
return self.styles.layout is not None or bool(self._nodes)
@property
def layer(self) -> str:

View File

@@ -94,33 +94,33 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
"""
if self.index is None:
return None
elif 0 <= self.index < len(self.children):
return self.children[self.index]
elif 0 <= self.index < len(self._nodes):
return self._nodes[self.index]
def validate_index(self, index: int | None) -> int | None:
"""Clamp the index to the valid range, or set to None if there's nothing to highlight."""
if not self.children or index is None:
if not self._nodes or index is None:
return None
return self._clamp_index(index)
def _clamp_index(self, index: int) -> int:
"""Clamp the index to a valid value given the current list of children"""
last_index = max(len(self.children) - 1, 0)
last_index = max(len(self._nodes) - 1, 0)
return clamp(index, 0, last_index)
def _is_valid_index(self, index: int | None) -> bool:
"""Return True if the current index is valid given the current list of children"""
if index is None:
return False
return 0 <= index < len(self.children)
return 0 <= index < len(self._nodes)
def watch_index(self, old_index: int, new_index: int) -> None:
"""Updates the highlighting when the index changes."""
if self._is_valid_index(old_index):
old_child = self.children[old_index]
old_child = self._nodes[old_index]
old_child.highlighted = False
if self._is_valid_index(new_index):
new_child = self.children[new_index]
new_child = self._nodes[new_index]
new_child.highlighted = True
else:
new_child = None
@@ -166,7 +166,7 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
def on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None:
self.focus()
self.index = self.children.index(event.sender)
self.index = self._nodes.index(event.sender)
self.post_message_no_wait(self.Selected(self, event.sender))
def _scroll_highlighted_region(self) -> None:
@@ -175,4 +175,4 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
self.scroll_to_widget(self.highlighted_child, animate=False)
def __len__(self):
return len(self.children)
return len(self._nodes)

View File

@@ -473,7 +473,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
text_label = self.process_label(label)
self._updates = 0
self._nodes: dict[NodeID, TreeNode[TreeDataType]] = {}
self._tree_nodes: dict[NodeID, TreeNode[TreeDataType]] = {}
self._current_id = 0
self.root = self._add_node(None, text_label, data)
@@ -515,7 +515,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
expand: bool = False,
) -> TreeNode[TreeDataType]:
node = TreeNode(self, parent, self._new_id(), label, data, expanded=expand)
self._nodes[node._id] = node
self._tree_nodes[node._id] = node
self._updates += 1
return node
@@ -630,7 +630,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
Tree.UnknownID: Raised if the `TreeNode` ID is unknown.
"""
try:
return self._nodes[node_id]
return self._tree_nodes[node_id]
except KeyError:
raise self.UnknownNodeID(f"Unknown NodeID ({node_id}) in tree") from None

View File

@@ -42,9 +42,11 @@ async def test_installed_screens():
pilot.app.pop_screen()
@skip_py310
async def test_screens():
app = App()
# There should be nothing in the children since the app hasn't run yet
assert not app._nodes
assert not app.children
app._set_active()
with pytest.raises(ScreenStackError):
@@ -60,6 +62,10 @@ async def test_screens():
app.install_screen(screen1, "screen1")
app.install_screen(screen2, "screen2")
# Installing a screen does not add it to the DOM
assert not app._nodes
assert not app.children
# Check they are installed
assert app.is_screen_installed("screen1")
assert app.is_screen_installed("screen2")
@@ -84,17 +90,22 @@ async def test_screens():
assert app.screen_stack == [screen1]
# Check it is current
assert app.screen is screen1
# There should be one item in the children view
assert app.children == (screen1,)
# Switch to another screen
app.switch_screen("screen2")
# Check it has changed the stack and that it is current
assert app.screen_stack == [screen2]
assert app.screen is screen2
assert app.children == (screen2,)
# Push another screen
app.push_screen("screen3")
assert app.screen_stack == [screen2, screen3]
assert app.screen is screen3
# Only the current screen is in children
assert app.children == (screen3,)
# Pop a screen
assert app.pop_screen() is screen3

View File

@@ -13,7 +13,7 @@ async def test_unmount():
class UnmountWidget(Container):
def on_unmount(self, event: events.Unmount):
unmount_ids.append(
f"{self.__class__.__name__}#{self.id}-{self.parent is not None}-{len(self.children)}"
f"{self.__class__.__name__}#{self.id}-{self.parent is not None}-{len(self._nodes)}"
)
class MyScreen(Screen):

View File

@@ -59,9 +59,9 @@ async def test_widget_move_child() -> None:
container = Widget(*widgets)
await pilot.app.mount(container)
container.move_child(child, before=target)
assert container.children[0].id == "widget-1"
assert container.children[1].id == "widget-0"
assert container.children[2].id == "widget-2"
assert container._nodes[0].id == "widget-1"
assert container._nodes[1].id == "widget-0"
assert container._nodes[2].id == "widget-2"
# Test the different permutations of moving one widget after another.
perms = ((0, 1), (widgets[0], 1), (0, widgets[1]), (widgets[0], widgets[1]))
@@ -70,22 +70,22 @@ async def test_widget_move_child() -> None:
container = Widget(*widgets)
await pilot.app.mount(container)
container.move_child(child, after=target)
assert container.children[0].id == "widget-1"
assert container.children[1].id == "widget-0"
assert container.children[2].id == "widget-2"
assert container._nodes[0].id == "widget-1"
assert container._nodes[1].id == "widget-0"
assert container._nodes[2].id == "widget-2"
# Test moving after a child after the last child.
async with App().run_test() as pilot:
container = Widget(*widgets)
await pilot.app.mount(container)
container.move_child(widgets[0], after=widgets[-1])
assert container.children[0].id == "widget-1"
assert container.children[-1].id == "widget-0"
assert container._nodes[0].id == "widget-1"
assert container._nodes[-1].id == "widget-0"
# Test moving after a child after the last child's numeric position.
async with App().run_test() as pilot:
container = Widget(*widgets)
await pilot.app.mount(container)
container.move_child(widgets[0], after=widgets[9])
assert container.children[0].id == "widget-1"
assert container.children[-1].id == "widget-0"
assert container._nodes[0].id == "widget-1"
assert container._nodes[-1].id == "widget-0"

View File

@@ -23,7 +23,7 @@ def test_find_dom_spot():
# Just as a quick double-check, make sure the main components are in
# their intended place.
assert list(screen.children) == [header, body, footer]
assert list(screen._nodes) == [header, body, footer]
# Now check that we find what we're looking for in the places we expect
# to find them.

View File

@@ -26,72 +26,72 @@ async def test_mount_via_app() -> None:
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]
assert len(pilot.app.screen._nodes) == 1
assert pilot.app.screen._nodes[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]
assert list(pilot.app.screen._nodes) == 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
assert list(pilot.app.screen._nodes) == 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
assert pilot.app.screen._nodes[-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
assert pilot.app.screen._nodes[-1] == ultimate
async with App().run_test() as pilot:
# Mount a widget before -2, which is "before the penultimate".
penpenultimate = Static(id="penpenultimate")
await pilot.app.mount_all(widgets)
await pilot.app.mount(penpenultimate, before=-2)
assert pilot.app.screen.children[-3] == penpenultimate
assert pilot.app.screen._nodes[-3] == penpenultimate
async with App().run_test() as pilot:
# Mount a widget after -2, which is "before the end".
penultimate = Static(id="penultimate")
await pilot.app.mount_all(widgets)
await pilot.app.mount(penultimate, after=-2)
assert pilot.app.screen.children[-2] == penultimate
assert pilot.app.screen._nodes[-2] == penultimate
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
assert pilot.app.screen._nodes[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
assert pilot.app.screen._nodes[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
assert pilot.app.screen._nodes[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
assert pilot.app.screen._nodes[6] == queue_jumper
async with App().run_test() as pilot:
# Make sure we get told off for trying to before and after.

View File

@@ -13,28 +13,28 @@ async def test_remove_single_widget():
assert not widget.is_attached
await pilot.app.mount(widget)
assert widget.is_attached
assert len(pilot.app.screen.children) == 1
assert len(pilot.app.screen._nodes) == 1
await pilot.app.query_one(Static).remove()
assert not widget.is_attached
assert len(pilot.app.screen.children) == 0
assert len(pilot.app.screen._nodes) == 0
async def test_many_remove_all_widgets():
"""It should be possible to remove all widgets on a multi-widget screen."""
async with App().run_test() as pilot:
await pilot.app.mount(*[Static() for _ in range(10)])
assert len(pilot.app.screen.children) == 10
assert len(pilot.app.screen._nodes) == 10
await pilot.app.query(Static).remove()
assert len(pilot.app.screen.children) == 0
assert len(pilot.app.screen._nodes) == 0
async def test_many_remove_some_widgets():
"""It should be possible to remove some widgets on a multi-widget screen."""
async with App().run_test() as pilot:
await pilot.app.mount(*[Static(classes=f"is-{n%2}") for n in range(10)])
assert len(pilot.app.screen.children) == 10
assert len(pilot.app.screen._nodes) == 10
await pilot.app.query(".is-0").remove()
assert len(pilot.app.screen.children) == 5
assert len(pilot.app.screen._nodes) == 5
async def test_remove_branch():
@@ -46,7 +46,7 @@ async def test_remove_branch():
Container(Container(Container(Container(Container(Static()))))),
)
assert len(pilot.app.screen.walk_children(with_self=False)) == 13
await pilot.app.screen.children[0].remove()
await pilot.app.screen._nodes[0].remove()
assert len(pilot.app.screen.walk_children(with_self=False)) == 7
@@ -68,14 +68,14 @@ async def test_remove_move_focus():
async with App().run_test() as pilot:
buttons = [Button(str(n)) for n in range(10)]
await pilot.app.mount(Container(*buttons[:5]), Container(*buttons[5:]))
assert len(pilot.app.screen.children) == 2
assert len(pilot.app.screen._nodes) == 2
assert len(pilot.app.screen.walk_children(with_self=False)) == 12
assert pilot.app.focused is None
await pilot.press("tab")
assert pilot.app.focused is not None
assert pilot.app.focused == buttons[0]
await pilot.app.screen.children[0].remove()
assert len(pilot.app.screen.children) == 1
await pilot.app.screen._nodes[0].remove()
assert len(pilot.app.screen._nodes) == 1
assert len(pilot.app.screen.walk_children(with_self=False)) == 6
assert pilot.app.focused is not None
assert pilot.app.focused == buttons[9]
@@ -95,7 +95,7 @@ async def test_widget_remove_order():
Removable(Removable(Removable(id="grandchild"), id="child"), id="parent")
)
assert len(pilot.app.screen.walk_children(with_self=False)) == 3
await pilot.app.screen.children[0].remove()
await pilot.app.screen._nodes[0].remove()
assert len(pilot.app.screen.walk_children(with_self=False)) == 0
assert removals == ["grandchild", "child", "parent"]