depth first search

This commit is contained in:
Will McGugan
2022-10-13 16:43:59 +01:00
parent 88447b78f6
commit 5a8e492294
6 changed files with 122 additions and 21 deletions

View File

@@ -0,0 +1,9 @@
Focusable {
padding: 3 6;
background: blue 20%;
}
Focusable :focus {
border: solid red;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App, ComposeResult
from textual.widgets import Static, Footer
class Focusable(Static, can_focus=True):
pass
class ScreensFocusApp(App):
def compose(self) -> ComposeResult:
yield Focusable("App - one")
yield Focusable("App - two")
yield Focusable("App - three")
yield Focusable("App - four")
yield Footer()
app = ScreensFocusApp(css_path="screens_focus.css")
if __name__ == "__main__":
app.run()

View File

@@ -1394,7 +1394,9 @@ class App(Generic[ReturnType], DOMNode):
if parent is not None:
parent.refresh(layout=True)
remove_widgets = list(widget.walk_children(Widget, with_self=True))
remove_widgets = list(
widget.walk_children(Widget, with_self=True, method="depth")
)
for child in remove_widgets:
self._unregister(child)
for child in remove_widgets:

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from collections import deque
from inspect import getfile
import re
import sys
from typing import (
cast,
ClassVar,
@@ -40,10 +42,23 @@ if TYPE_CHECKING:
from .screen import Screen
from .widget import Widget
if sys.version_info >= (3, 8):
from typing import Literal, Iterable, Sequence
else:
from typing_extensions import Literal
if sys.version_info >= (3, 10):
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
_re_identifier = re.compile(IDENTIFIER)
WalkMethod: TypeAlias = Literal["depth", "breadth"]
class BadIdentifier(Exception):
"""raised by check_identifiers."""
@@ -617,11 +632,14 @@ class DOMNode(MessagePump):
filter_type: type[WalkType],
*,
with_self: bool = True,
method: WalkMethod = "breadth",
) -> Iterable[WalkType]:
...
@overload
def walk_children(self, *, with_self: bool = True) -> Iterable[DOMNode]:
def walk_children(
self, *, with_self: bool = True, method: WalkMethod = "breadth"
) -> Iterable[DOMNode]:
...
def walk_children(
@@ -629,6 +647,7 @@ class DOMNode(MessagePump):
filter_type: type[WalkType] | None = None,
*,
with_self: bool = True,
method: WalkMethod = "breadth",
) -> Iterable[DOMNode | WalkType]:
"""Generate descendant nodes.
@@ -636,29 +655,58 @@ class DOMNode(MessagePump):
filter_type (type[WalkType] | None, optional): Filter only this type, or None for no filter.
Defaults to None.
with_self (bool, optional): Also yield self in addition to descendants. Defaults to True.
method (Literal["breadth", "depth"], optional): One of "depth" or "breadth". Defaults to "breadth".
Returns:
Iterable[DOMNode | WalkType]: An iterable of nodes.
"""
stack: list[Iterator[DOMNode]] = [iter(self.children)]
pop = stack.pop
push = stack.append
check_type = filter_type or DOMNode
def walk_breadth_first() -> Iterable[DOMNode]:
"""Walk the tree breadth first (parent's first)."""
stack: list[Iterator[DOMNode]] = [iter(self.children)]
pop = stack.pop
push = stack.append
check_type = filter_type or DOMNode
if with_self and isinstance(self, check_type):
yield self
if with_self and isinstance(self, check_type):
yield self
while stack:
node = next(stack[-1], None)
if node is None:
pop()
else:
if isinstance(node, check_type):
yield node
if node.children:
push(iter(node.children))
while stack:
node = next(stack[-1], None)
if node is None:
pop()
else:
if isinstance(node, check_type):
yield node
if node.children:
push(iter(node.children))
def walk_depth_first() -> Iterable[DOMNode]:
"""Walk the tree depth first (children first)."""
depth_stack: list[tuple[DOMNode, Iterator[DOMNode]]] = (
[(self, iter(self.children))]
if with_self
else [(node, iter(node.children)) for node in reversed(self.children)]
)
pop = depth_stack.pop
push = depth_stack.append
check_type = filter_type or DOMNode
while depth_stack:
node, iter_nodes = pop()
child_widget = next(iter_nodes, None)
if child_widget is None:
if isinstance(node, check_type):
yield node
else:
push((node, iter_nodes))
push((child_widget, iter(child_widget.children)))
if method == "depth":
yield from walk_depth_first()
else:
yield from walk_breadth_first()
def get_child(self, id: str) -> DOMNode:
"""Return the first child (immediate descendent) of this node with the given ID.

View File

@@ -1857,10 +1857,6 @@ class Widget(DOMNode):
await self.action(binding.action)
return True
def _on_compose(self, event: events.Compose) -> None:
widgets = self.compose()
self.app.mount_all(widgets)
def _on_mount(self, event: events.Mount) -> None:
widgets = self.compose()
self.mount(*widgets)

View File

@@ -75,3 +75,29 @@ def test_validate():
node.remove_class("1")
with pytest.raises(BadIdentifier):
node.toggle_class("1")
def test_walk_children(parent):
children = [node.id for node in parent.walk_children(with_self=False)]
assert children == ["child1", "grandchild1", "child2"]
def test_walk_children_with_self(parent):
children = [node.id for node in parent.walk_children(with_self=True)]
assert children == ["parent", "child1", "grandchild1", "child2"]
def test_walk_children_depth(parent):
children = [
node.id for node in parent.walk_children(with_self=False, method="depth")
]
print(children)
assert children == ["grandchild1", "child1", "child2"]
def test_walk_children_with_self_depth(parent):
children = [
node.id for node in parent.walk_children(with_self=True, method="depth")
]
print(children)
assert children == ["grandchild1", "child1", "child2", "parent"]