Skip to content

Commit 29f7adc

Browse files
committed
change to app query model
1 parent 7c25ab5 commit 29f7adc

File tree

14 files changed

+70
-64
lines changed

14 files changed

+70
-64
lines changed

src/textual/app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,9 @@ def __init__(
798798
self.supports_smooth_scrolling: bool = False
799799
"""Does the terminal support smooth scrolling?"""
800800

801+
self._compose_screen: Screen | None = None
802+
"""The screen composed by App.compose."""
803+
801804
if self.ENABLE_COMMAND_PALETTE:
802805
for _key, binding in self._bindings:
803806
if binding.action in {"command_palette", "app.command_palette"}:
@@ -833,6 +836,9 @@ def __init_subclass__(cls, *args, **kwargs) -> None:
833836

834837
return super().__init_subclass__(*args, **kwargs)
835838

839+
def _get_dom_base(self) -> DOMNode:
840+
return self.screen if self._compose_screen is None else self._compose_screen
841+
836842
def validate_title(self, title: Any) -> str:
837843
"""Make sure the title is set to a string."""
838844
return str(title)
@@ -3237,6 +3243,7 @@ async def take_screenshot() -> None:
32373243

32383244
async def _on_compose(self) -> None:
32393245
_rich_traceback_omit = True
3246+
self._compose_screen = self.screen
32403247
try:
32413248
widgets = [*self.screen._nodes, *compose(self)]
32423249
except TypeError as error:

src/textual/content.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class Content(Visual):
119119

120120
def __init__(
121121
self,
122-
text: str,
122+
text: str = "",
123123
spans: list[Span] | None = None,
124124
cell_length: int | None = None,
125125
) -> None:

src/textual/dom.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ def __init__(
235235

236236
super().__init__()
237237

238+
def _get_dom_base(self) -> DOMNode:
239+
"""Get the DOM base node (typically self).
240+
241+
Returns:
242+
DOMNode.
243+
"""
244+
return self
245+
238246
def set_reactive(
239247
self, reactive: Reactive[ReactiveType], value: ReactiveType
240248
) -> None:
@@ -1380,10 +1388,11 @@ def query(
13801388
from textual.css.query import DOMQuery, QueryType
13811389
from textual.widget import Widget
13821390

1391+
node = self._get_dom_base()
13831392
if isinstance(selector, str) or selector is None:
1384-
return DOMQuery[Widget](self, filter=selector)
1393+
return DOMQuery[Widget](node, filter=selector)
13851394
else:
1386-
return DOMQuery[QueryType](self, filter=selector.__name__)
1395+
return DOMQuery[QueryType](node, filter=selector.__name__)
13871396

13881397
if TYPE_CHECKING:
13891398

@@ -1411,10 +1420,11 @@ def query_children(
14111420
from textual.css.query import DOMQuery, QueryType
14121421
from textual.widget import Widget
14131422

1423+
node = self._get_dom_base()
14141424
if isinstance(selector, str) or selector is None:
1415-
return DOMQuery[Widget](self, deep=False, filter=selector)
1425+
return DOMQuery[Widget](node, deep=False, filter=selector)
14161426
else:
1417-
return DOMQuery[QueryType](self, deep=False, filter=selector.__name__)
1427+
return DOMQuery[QueryType](node, deep=False, filter=selector.__name__)
14181428

14191429
if TYPE_CHECKING:
14201430

@@ -1449,6 +1459,8 @@ def query_one(
14491459
"""
14501460
_rich_traceback_omit = True
14511461

1462+
base_node = self._get_dom_base()
1463+
14521464
if isinstance(selector, str):
14531465
query_selector = selector
14541466
else:
@@ -1462,20 +1474,20 @@ def query_one(
14621474
) from None
14631475

14641476
if all(selectors.is_simple for selectors in selector_set):
1465-
cache_key = (self._nodes._updates, query_selector, expect_type)
1466-
cached_result = self._query_one_cache.get(cache_key)
1477+
cache_key = (base_node._nodes._updates, query_selector, expect_type)
1478+
cached_result = base_node._query_one_cache.get(cache_key)
14671479
if cached_result is not None:
14681480
return cached_result
14691481
else:
14701482
cache_key = None
14711483

1472-
for node in walk_depth_first(self, with_root=False):
1484+
for node in walk_depth_first(base_node, with_root=False):
14731485
if not match(selector_set, node):
14741486
continue
14751487
if expect_type is not None and not isinstance(node, expect_type):
14761488
continue
14771489
if cache_key is not None:
1478-
self._query_one_cache[cache_key] = node
1490+
base_node._query_one_cache[cache_key] = node
14791491
return node
14801492

14811493
raise NoMatches(f"No nodes match {selector!r} on {self!r}")
@@ -1518,6 +1530,8 @@ def query_exactly_one(
15181530
"""
15191531
_rich_traceback_omit = True
15201532

1533+
base_node = self._get_dom_base()
1534+
15211535
if isinstance(selector, str):
15221536
query_selector = selector
15231537
else:
@@ -1531,14 +1545,14 @@ def query_exactly_one(
15311545
) from None
15321546

15331547
if all(selectors.is_simple for selectors in selector_set):
1534-
cache_key = (self._nodes._updates, query_selector, expect_type)
1535-
cached_result = self._query_one_cache.get(cache_key)
1548+
cache_key = (base_node._nodes._updates, query_selector, expect_type)
1549+
cached_result = base_node._query_one_cache.get(cache_key)
15361550
if cached_result is not None:
15371551
return cached_result
15381552
else:
15391553
cache_key = None
15401554

1541-
children = walk_depth_first(self, with_root=False)
1555+
children = walk_depth_first(base_node, with_root=False)
15421556
iter_children = iter(children)
15431557
for node in iter_children:
15441558
if not match(selector_set, node):
@@ -1553,7 +1567,7 @@ def query_exactly_one(
15531567
"Call to query_one resulted in more than one matched node"
15541568
)
15551569
if cache_key is not None:
1556-
self._query_one_cache[cache_key] = node
1570+
base_node._query_one_cache[cache_key] = node
15571571
return node
15581572

15591573
raise NoMatches(f"No nodes match {selector!r} on {self!r}")
@@ -1589,6 +1603,7 @@ def query_ancestor(
15891603
Returns:
15901604
A DOMNode or subclass if `expect_type` is provided.
15911605
"""
1606+
base_node = self._get_dom_base()
15921607
if isinstance(selector, str):
15931608
query_selector = selector
15941609
else:
@@ -1600,8 +1615,8 @@ def query_ancestor(
16001615
raise InvalidQueryFormat(
16011616
f"Unable to parse {query_selector!r} as a query; check for syntax errors"
16021617
) from None
1603-
if self.parent is not None:
1604-
for node in self.parent.ancestors_with_self:
1618+
if base_node.parent is not None:
1619+
for node in base_node.parent.ancestors_with_self:
16051620
if not match(selector_set, node):
16061621
continue
16071622
if expect_type is not None and not isinstance(node, expect_type):

src/textual/pilot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ async def _post_mouse_events(
414414
elif isinstance(widget, Widget):
415415
target_widget = widget
416416
else:
417-
target_widget = app.query_one(widget)
417+
target_widget = app.screen.query_one(widget)
418418

419419
message_arguments = _get_mouse_message_arguments(
420420
target_widget,

tests/css/test_screen_css.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,15 @@ def on_mount(self):
5858

5959

6060
def check_colors_before_screen_css(app: BaseApp):
61-
assert app.query_one("#app-css").styles.background == GREEN
62-
assert app.query_one("#screen-css-path").styles.background == GREEN
63-
assert app.query_one("#screen-css").styles.background == GREEN
61+
assert app.screen.query_one("#app-css").styles.background == GREEN
62+
assert app.screen.query_one("#screen-css-path").styles.background == GREEN
63+
assert app.screen.query_one("#screen-css").styles.background == GREEN
6464

6565

6666
def check_colors_after_screen_css(app: BaseApp):
67-
assert app.query_one("#app-css").styles.background == GREEN
68-
assert app.query_one("#screen-css-path").styles.background == BLUE
69-
assert app.query_one("#screen-css").styles.background == RED
67+
assert app.screen.query_one("#app-css").styles.background == GREEN
68+
assert app.screen.query_one("#screen-css-path").styles.background == BLUE
69+
assert app.screen.query_one("#screen-css").styles.background == RED
7070

7171

7272
async def test_screen_pushing_and_popping_does_not_reparse_css():

tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def compose(self) -> ComposeResult:
1111
yield Footer()
1212

1313
def on_mount(self) -> None:
14-
self.query_one("Screen").scroll_end()
14+
self.screen.scroll_end()
1515

1616

1717
app = ScrollOffByOne()

tests/test_compositor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def compose(self) -> ComposeResult:
3030
yield Static("Hello", id="hello")
3131

3232
def on_mount(self) -> None:
33-
self.query_one("Screen").scroll_to(20, 0, animate=False)
33+
self.screen.scroll_to(20, 0, animate=False)
3434

3535
app = ScrollApp()
3636
async with app.run_test() as pilot:

tests/test_header.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def on_mount(self):
1616

1717
app = MyApp()
1818
async with app.run_test():
19-
assert app.query_one("HeaderTitle").text == "app title"
19+
assert app.screen.query_one("HeaderTitle").text == "app title"
2020

2121

2222
async def test_screen_title_overrides_app_title():
@@ -34,7 +34,7 @@ def on_mount(self):
3434

3535
app = MyApp()
3636
async with app.run_test():
37-
assert app.query_one("HeaderTitle").text == "screen title"
37+
assert app.screen.query_one("HeaderTitle").text == "screen title"
3838

3939

4040
async def test_screen_title_reactive_updates_title():
@@ -54,7 +54,7 @@ def on_mount(self):
5454
async with app.run_test() as pilot:
5555
app.screen.title = "new screen title"
5656
await pilot.pause()
57-
assert app.query_one("HeaderTitle").text == "new screen title"
57+
assert app.screen.query_one("HeaderTitle").text == "new screen title"
5858

5959

6060
async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set():
@@ -74,7 +74,7 @@ def on_mount(self):
7474
async with app.run_test() as pilot:
7575
app.title = "new app title"
7676
await pilot.pause()
77-
assert app.query_one("HeaderTitle").text == "screen title"
77+
assert app.screen.query_one("HeaderTitle").text == "screen title"
7878

7979

8080
async def test_screen_sub_title_none_is_ignored():
@@ -90,7 +90,7 @@ def on_mount(self):
9090

9191
app = MyApp()
9292
async with app.run_test():
93-
assert app.query_one("HeaderTitle").sub_text == "app sub-title"
93+
assert app.screen.query_one("HeaderTitle").sub_text == "app sub-title"
9494

9595

9696
async def test_screen_sub_title_overrides_app_sub_title():
@@ -108,7 +108,7 @@ def on_mount(self):
108108

109109
app = MyApp()
110110
async with app.run_test():
111-
assert app.query_one("HeaderTitle").sub_text == "screen sub-title"
111+
assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title"
112112

113113

114114
async def test_screen_sub_title_reactive_updates_sub_title():
@@ -128,7 +128,7 @@ def on_mount(self):
128128
async with app.run_test() as pilot:
129129
app.screen.sub_title = "new screen sub-title"
130130
await pilot.pause()
131-
assert app.query_one("HeaderTitle").sub_text == "new screen sub-title"
131+
assert app.screen.query_one("HeaderTitle").sub_text == "new screen sub-title"
132132

133133

134134
async def test_app_sub_title_reactive_does_not_update_sub_title_when_screen_sub_title_is_set():
@@ -148,4 +148,4 @@ def on_mount(self):
148148
async with app.run_test() as pilot:
149149
app.sub_title = "new app sub-title"
150150
await pilot.pause()
151-
assert app.query_one("HeaderTitle").sub_text == "screen sub-title"
151+
assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title"

tests/test_pilot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async def test_pilot_click_screen():
126126
Check we can use `Screen` as a selector for a click."""
127127

128128
async with App().run_test() as pilot:
129-
await pilot.click("Screen")
129+
await pilot.click()
130130

131131

132132
async def test_pilot_hover_screen():
@@ -135,7 +135,7 @@ async def test_pilot_hover_screen():
135135
Check we can use `Screen` as a selector for a hover."""
136136

137137
async with App().run_test() as pilot:
138-
await pilot.hover("Screen")
138+
await pilot.hover()
139139

140140

141141
@pytest.mark.parametrize(

tests/test_query.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@
1414
from textual.widgets import Input, Label
1515

1616

17-
def test_query_errors():
17+
async def test_query_errors():
1818
app = App()
19+
async with app.run_test():
20+
with pytest.raises(InvalidQueryFormat):
21+
app.query_one("foo_bar")
1922

20-
with pytest.raises(InvalidQueryFormat):
21-
app.query_one("foo_bar")
22-
23-
with pytest.raises(InvalidQueryFormat):
24-
app.query("foo_bar")
23+
with pytest.raises(InvalidQueryFormat):
24+
app.query("foo_bar")
2525

26-
with pytest.raises(InvalidQueryFormat):
27-
app.query("1")
26+
with pytest.raises(InvalidQueryFormat):
27+
app.query("1")
2828

29-
with pytest.raises(InvalidQueryFormat):
30-
app.query_one("1")
29+
with pytest.raises(InvalidQueryFormat):
30+
app.query_one("1")
3131

3232

3333
def test_query():

0 commit comments

Comments
 (0)