Skip to content

Commit a4aa30e

Browse files
authored
Merge pull request #1416 from Textualize/query-star-exclude-self
Queries/`walk_children` no longer includes self in results by default
2 parents 9fbad7f + 3514574 commit a4aa30e

File tree

6 files changed

+52
-27
lines changed

6 files changed

+52
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2121
### Changed
2222

2323
- Moved Ctrl+C, tab, and shift+tab to App BINDINGS
24+
- Queries/`walk_children` no longer includes self in results by default https://github.com/Textualize/textual/pull/1416
2425

2526
## [0.7.0] - 2022-12-17
2627

src/textual/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@ def _on_idle(self) -> None:
14981498
nodes: set[DOMNode] = {
14991499
child
15001500
for node in self._require_stylesheet_update
1501-
for child in node.walk_children()
1501+
for child in node.walk_children(with_self=True)
15021502
}
15031503
self._require_stylesheet_update.clear()
15041504
self.stylesheet.update_nodes(nodes, animate=True)

src/textual/css/match.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ def match(selector_sets: Iterable[SelectorSet], node: DOMNode) -> bool:
2525

2626

2727
def _check_selectors(selectors: list[Selector], css_path_nodes: list[DOMNode]) -> bool:
28-
"""Match a list of selectors against a node.
28+
"""Match a list of selectors against DOM nodes.
2929
3030
Args:
3131
selectors (list[Selector]): A list of selectors.
32-
node (DOMNode): A DOM node.
32+
css_path_nodes (list[DOMNode]): The DOM nodes to check the selectors against.
3333
3434
Returns:
35-
bool: True if the node matches the selector.
35+
bool: True if any node in css_path_nodes matches a selector.
3636
"""
3737

3838
DESCENDENT = CombinatorType.DESCENDENT

src/textual/css/stylesheet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ def update(self, root: DOMNode, animate: bool = False) -> None:
490490
animate (bool, optional): Enable CSS animation. Defaults to False.
491491
"""
492492

493-
self.update_nodes(root.walk_children(), animate=animate)
493+
self.update_nodes(root.walk_children(with_self=True), animate=animate)
494494

495495
def update_nodes(self, nodes: Iterable[DOMNode], animate: bool = False) -> None:
496496
"""Update styles for nodes.

src/textual/dom.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ def reset_styles(self) -> None:
615615
"""Reset styles back to their initial state"""
616616
from .widget import Widget
617617

618-
for node in self.walk_children():
618+
for node in self.walk_children(with_self=True):
619619
node._css_styles.reset()
620620
if isinstance(node, Widget):
621621
node._set_dirty()
@@ -648,7 +648,7 @@ def walk_children(
648648
self,
649649
filter_type: type[WalkType],
650650
*,
651-
with_self: bool = True,
651+
with_self: bool = False,
652652
method: WalkMethod = "depth",
653653
reverse: bool = False,
654654
) -> list[WalkType]:
@@ -658,7 +658,7 @@ def walk_children(
658658
def walk_children(
659659
self,
660660
*,
661-
with_self: bool = True,
661+
with_self: bool = False,
662662
method: WalkMethod = "depth",
663663
reverse: bool = False,
664664
) -> list[DOMNode]:
@@ -668,16 +668,16 @@ def walk_children(
668668
self,
669669
filter_type: type[WalkType] | None = None,
670670
*,
671-
with_self: bool = True,
671+
with_self: bool = False,
672672
method: WalkMethod = "depth",
673673
reverse: bool = False,
674674
) -> list[DOMNode] | list[WalkType]:
675-
"""Generate descendant nodes.
675+
"""Walk the subtree rooted at this node, and return every descendant encountered in a list.
676676
677677
Args:
678678
filter_type (type[WalkType] | None, optional): Filter only this type, or None for no filter.
679679
Defaults to None.
680-
with_self (bool, optional): Also yield self in addition to descendants. Defaults to True.
680+
with_self (bool, optional): Also yield self in addition to descendants. Defaults to False.
681681
method (Literal["breadth", "depth"], optional): One of "depth" or "breadth". Defaults to "depth".
682682
reverse (bool, optional): Reverse the order (bottom up). Defaults to False.
683683

tests/test_query.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22

3+
from textual.app import App, ComposeResult
4+
from textual.containers import Container
35
from textual.widget import Widget
46
from textual.css.query import InvalidQueryFormat, WrongType, NoMatches, TooManyMatches
57

@@ -50,17 +52,16 @@ class App(Widget):
5052
assert list(app.query(".frob")) == []
5153
assert list(app.query("#frob")) == []
5254

53-
assert app.query("App")
5455
assert not app.query("NotAnApp")
5556

56-
assert list(app.query("App")) == [app]
57+
assert list(app.query("App")) == [] # Root is not included in queries
5758
assert list(app.query("#main")) == [main_view]
5859
assert list(app.query("View#main")) == [main_view]
5960
assert list(app.query("View2#help")) == [help_view]
6061
assert list(app.query("#widget1")) == [widget1]
61-
assert list(app.query("#Widget1")) == [] # Note case.
62+
assert list(app.query("#Widget1")) == [] # Note case.
6263
assert list(app.query("#widget2")) == [widget2]
63-
assert list(app.query("#Widget2")) == [] # Note case.
64+
assert list(app.query("#Widget2")) == [] # Note case.
6465

6566
assert list(app.query("Widget.float")) == [sidebar, tooltip, helpbar]
6667
assert list(app.query(Widget).filter(".float")) == [sidebar, tooltip, helpbar]
@@ -110,10 +111,10 @@ class App(Widget):
110111
tooltip,
111112
]
112113

113-
assert list(app.query("App,View")) == [app, main_view, sub_view, help_view]
114+
assert list(app.query("View")) == [main_view, sub_view, help_view]
114115
assert list(app.query("#widget1, #widget2")) == [widget1, widget2]
115116
assert list(app.query("#widget1 , #widget2")) == [widget1, widget2]
116-
assert list(app.query("#widget1, #widget2, App")) == [app, widget1, widget2]
117+
assert list(app.query("#widget1, #widget2, App")) == [widget1, widget2]
117118

118119
assert app.query(".float").first() == sidebar
119120
assert app.query(".float").last() == helpbar
@@ -130,14 +131,13 @@ class App(Widget):
130131

131132

132133
def test_query_classes():
133-
134134
class App(Widget):
135135
pass
136136

137137
class ClassTest(Widget):
138138
pass
139139

140-
CHILD_COUNT=100
140+
CHILD_COUNT = 100
141141

142142
# Create a fake app to hold everything else.
143143
app = App()
@@ -147,34 +147,35 @@ class ClassTest(Widget):
147147
app._add_child(ClassTest(id=f"child{n}"))
148148

149149
# Let's just be 100% sure everything was created fine.
150-
assert len(app.query(ClassTest))==CHILD_COUNT
150+
assert len(app.query(ClassTest)) == CHILD_COUNT
151151

152152
# Now, let's check there are *no* children with the test class.
153-
assert len(app.query(".test"))==0
153+
assert len(app.query(".test")) == 0
154154

155155
# Add the test class to everything and then check again.
156156
app.query(ClassTest).add_class("test")
157-
assert len(app.query(".test"))==CHILD_COUNT
157+
assert len(app.query(".test")) == CHILD_COUNT
158158

159159
# Remove the test class from everything then try again.
160160
app.query(ClassTest).remove_class("test")
161-
assert len(app.query(".test"))==0
161+
assert len(app.query(".test")) == 0
162162

163163
# Add the test class to everything using set_class.
164164
app.query(ClassTest).set_class(True, "test")
165-
assert len(app.query(".test"))==CHILD_COUNT
165+
assert len(app.query(".test")) == CHILD_COUNT
166166

167167
# Remove the test class from everything using set_class.
168168
app.query(ClassTest).set_class(False, "test")
169-
assert len(app.query(".test"))==0
169+
assert len(app.query(".test")) == 0
170170

171171
# Add the test class to everything using toggle_class.
172172
app.query(ClassTest).toggle_class("test")
173-
assert len(app.query(".test"))==CHILD_COUNT
173+
assert len(app.query(".test")) == CHILD_COUNT
174174

175175
# Remove the test class from everything using toggle_class.
176176
app.query(ClassTest).toggle_class("test")
177-
assert len(app.query(".test"))==0
177+
assert len(app.query(".test")) == 0
178+
178179

179180
def test_invalid_query():
180181
class App(Widget):
@@ -187,3 +188,26 @@ class App(Widget):
187188

188189
with pytest.raises(InvalidQueryFormat):
189190
app.query("#foo").exclude("#2")
191+
192+
193+
async def test_universal_selector_doesnt_select_self():
194+
class ExampleApp(App):
195+
def compose(self) -> ComposeResult:
196+
yield Container(
197+
Widget(
198+
Widget(),
199+
Widget(
200+
Widget(),
201+
),
202+
),
203+
Widget(),
204+
id="root-container",
205+
)
206+
207+
app = ExampleApp()
208+
async with app.run_test():
209+
container = app.query_one("#root-container", Container)
210+
query = container.query("*")
211+
results = list(query.results())
212+
assert len(results) == 5
213+
assert not any(node.id == "root-container" for node in results)

0 commit comments

Comments
 (0)