mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
change to app query model
This commit is contained in:
@@ -798,6 +798,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.supports_smooth_scrolling: bool = False
|
||||
"""Does the terminal support smooth scrolling?"""
|
||||
|
||||
self._compose_screen: Screen | None = None
|
||||
"""The screen composed by App.compose."""
|
||||
|
||||
if self.ENABLE_COMMAND_PALETTE:
|
||||
for _key, binding in self._bindings:
|
||||
if binding.action in {"command_palette", "app.command_palette"}:
|
||||
@@ -833,6 +836,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
return super().__init_subclass__(*args, **kwargs)
|
||||
|
||||
def _get_dom_base(self) -> DOMNode:
|
||||
return self.screen if self._compose_screen is None else self._compose_screen
|
||||
|
||||
def validate_title(self, title: Any) -> str:
|
||||
"""Make sure the title is set to a string."""
|
||||
return str(title)
|
||||
@@ -3237,6 +3243,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
async def _on_compose(self) -> None:
|
||||
_rich_traceback_omit = True
|
||||
self._compose_screen = self.screen
|
||||
try:
|
||||
widgets = [*self.screen._nodes, *compose(self)]
|
||||
except TypeError as error:
|
||||
|
||||
@@ -119,7 +119,7 @@ class Content(Visual):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
text: str = "",
|
||||
spans: list[Span] | None = None,
|
||||
cell_length: int | None = None,
|
||||
) -> None:
|
||||
|
||||
@@ -235,6 +235,14 @@ class DOMNode(MessagePump):
|
||||
|
||||
super().__init__()
|
||||
|
||||
def _get_dom_base(self) -> DOMNode:
|
||||
"""Get the DOM base node (typically self).
|
||||
|
||||
Returns:
|
||||
DOMNode.
|
||||
"""
|
||||
return self
|
||||
|
||||
def set_reactive(
|
||||
self, reactive: Reactive[ReactiveType], value: ReactiveType
|
||||
) -> None:
|
||||
@@ -1380,10 +1388,11 @@ class DOMNode(MessagePump):
|
||||
from textual.css.query import DOMQuery, QueryType
|
||||
from textual.widget import Widget
|
||||
|
||||
node = self._get_dom_base()
|
||||
if isinstance(selector, str) or selector is None:
|
||||
return DOMQuery[Widget](self, filter=selector)
|
||||
return DOMQuery[Widget](node, filter=selector)
|
||||
else:
|
||||
return DOMQuery[QueryType](self, filter=selector.__name__)
|
||||
return DOMQuery[QueryType](node, filter=selector.__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1411,10 +1420,11 @@ class DOMNode(MessagePump):
|
||||
from textual.css.query import DOMQuery, QueryType
|
||||
from textual.widget import Widget
|
||||
|
||||
node = self._get_dom_base()
|
||||
if isinstance(selector, str) or selector is None:
|
||||
return DOMQuery[Widget](self, deep=False, filter=selector)
|
||||
return DOMQuery[Widget](node, deep=False, filter=selector)
|
||||
else:
|
||||
return DOMQuery[QueryType](self, deep=False, filter=selector.__name__)
|
||||
return DOMQuery[QueryType](node, deep=False, filter=selector.__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1449,6 +1459,8 @@ class DOMNode(MessagePump):
|
||||
"""
|
||||
_rich_traceback_omit = True
|
||||
|
||||
base_node = self._get_dom_base()
|
||||
|
||||
if isinstance(selector, str):
|
||||
query_selector = selector
|
||||
else:
|
||||
@@ -1462,20 +1474,20 @@ class DOMNode(MessagePump):
|
||||
) from None
|
||||
|
||||
if all(selectors.is_simple for selectors in selector_set):
|
||||
cache_key = (self._nodes._updates, query_selector, expect_type)
|
||||
cached_result = self._query_one_cache.get(cache_key)
|
||||
cache_key = (base_node._nodes._updates, query_selector, expect_type)
|
||||
cached_result = base_node._query_one_cache.get(cache_key)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
else:
|
||||
cache_key = None
|
||||
|
||||
for node in walk_depth_first(self, with_root=False):
|
||||
for node in walk_depth_first(base_node, with_root=False):
|
||||
if not match(selector_set, node):
|
||||
continue
|
||||
if expect_type is not None and not isinstance(node, expect_type):
|
||||
continue
|
||||
if cache_key is not None:
|
||||
self._query_one_cache[cache_key] = node
|
||||
base_node._query_one_cache[cache_key] = node
|
||||
return node
|
||||
|
||||
raise NoMatches(f"No nodes match {selector!r} on {self!r}")
|
||||
@@ -1518,6 +1530,8 @@ class DOMNode(MessagePump):
|
||||
"""
|
||||
_rich_traceback_omit = True
|
||||
|
||||
base_node = self._get_dom_base()
|
||||
|
||||
if isinstance(selector, str):
|
||||
query_selector = selector
|
||||
else:
|
||||
@@ -1531,14 +1545,14 @@ class DOMNode(MessagePump):
|
||||
) from None
|
||||
|
||||
if all(selectors.is_simple for selectors in selector_set):
|
||||
cache_key = (self._nodes._updates, query_selector, expect_type)
|
||||
cached_result = self._query_one_cache.get(cache_key)
|
||||
cache_key = (base_node._nodes._updates, query_selector, expect_type)
|
||||
cached_result = base_node._query_one_cache.get(cache_key)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
else:
|
||||
cache_key = None
|
||||
|
||||
children = walk_depth_first(self, with_root=False)
|
||||
children = walk_depth_first(base_node, with_root=False)
|
||||
iter_children = iter(children)
|
||||
for node in iter_children:
|
||||
if not match(selector_set, node):
|
||||
@@ -1553,7 +1567,7 @@ class DOMNode(MessagePump):
|
||||
"Call to query_one resulted in more than one matched node"
|
||||
)
|
||||
if cache_key is not None:
|
||||
self._query_one_cache[cache_key] = node
|
||||
base_node._query_one_cache[cache_key] = node
|
||||
return node
|
||||
|
||||
raise NoMatches(f"No nodes match {selector!r} on {self!r}")
|
||||
@@ -1589,6 +1603,7 @@ class DOMNode(MessagePump):
|
||||
Returns:
|
||||
A DOMNode or subclass if `expect_type` is provided.
|
||||
"""
|
||||
base_node = self._get_dom_base()
|
||||
if isinstance(selector, str):
|
||||
query_selector = selector
|
||||
else:
|
||||
@@ -1600,8 +1615,8 @@ class DOMNode(MessagePump):
|
||||
raise InvalidQueryFormat(
|
||||
f"Unable to parse {query_selector!r} as a query; check for syntax errors"
|
||||
) from None
|
||||
if self.parent is not None:
|
||||
for node in self.parent.ancestors_with_self:
|
||||
if base_node.parent is not None:
|
||||
for node in base_node.parent.ancestors_with_self:
|
||||
if not match(selector_set, node):
|
||||
continue
|
||||
if expect_type is not None and not isinstance(node, expect_type):
|
||||
|
||||
@@ -414,7 +414,7 @@ class Pilot(Generic[ReturnType]):
|
||||
elif isinstance(widget, Widget):
|
||||
target_widget = widget
|
||||
else:
|
||||
target_widget = app.query_one(widget)
|
||||
target_widget = app.screen.query_one(widget)
|
||||
|
||||
message_arguments = _get_mouse_message_arguments(
|
||||
target_widget,
|
||||
|
||||
@@ -58,15 +58,15 @@ class SwitchBaseApp(BaseApp):
|
||||
|
||||
|
||||
def check_colors_before_screen_css(app: BaseApp):
|
||||
assert app.query_one("#app-css").styles.background == GREEN
|
||||
assert app.query_one("#screen-css-path").styles.background == GREEN
|
||||
assert app.query_one("#screen-css").styles.background == GREEN
|
||||
assert app.screen.query_one("#app-css").styles.background == GREEN
|
||||
assert app.screen.query_one("#screen-css-path").styles.background == GREEN
|
||||
assert app.screen.query_one("#screen-css").styles.background == GREEN
|
||||
|
||||
|
||||
def check_colors_after_screen_css(app: BaseApp):
|
||||
assert app.query_one("#app-css").styles.background == GREEN
|
||||
assert app.query_one("#screen-css-path").styles.background == BLUE
|
||||
assert app.query_one("#screen-css").styles.background == RED
|
||||
assert app.screen.query_one("#app-css").styles.background == GREEN
|
||||
assert app.screen.query_one("#screen-css-path").styles.background == BLUE
|
||||
assert app.screen.query_one("#screen-css").styles.background == RED
|
||||
|
||||
|
||||
async def test_screen_pushing_and_popping_does_not_reparse_css():
|
||||
|
||||
@@ -11,7 +11,7 @@ class ScrollOffByOne(App):
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.query_one("Screen").scroll_end()
|
||||
self.screen.scroll_end()
|
||||
|
||||
|
||||
app = ScrollOffByOne()
|
||||
|
||||
@@ -30,7 +30,7 @@ async def test_compositor_scroll_placements():
|
||||
yield Static("Hello", id="hello")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.query_one("Screen").scroll_to(20, 0, animate=False)
|
||||
self.screen.scroll_to(20, 0, animate=False)
|
||||
|
||||
app = ScrollApp()
|
||||
async with app.run_test() as pilot:
|
||||
|
||||
@@ -16,7 +16,7 @@ async def test_screen_title_none_is_ignored():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.query_one("HeaderTitle").text == "app title"
|
||||
assert app.screen.query_one("HeaderTitle").text == "app title"
|
||||
|
||||
|
||||
async def test_screen_title_overrides_app_title():
|
||||
@@ -34,7 +34,7 @@ async def test_screen_title_overrides_app_title():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.query_one("HeaderTitle").text == "screen title"
|
||||
assert app.screen.query_one("HeaderTitle").text == "screen title"
|
||||
|
||||
|
||||
async def test_screen_title_reactive_updates_title():
|
||||
@@ -54,7 +54,7 @@ async def test_screen_title_reactive_updates_title():
|
||||
async with app.run_test() as pilot:
|
||||
app.screen.title = "new screen title"
|
||||
await pilot.pause()
|
||||
assert app.query_one("HeaderTitle").text == "new screen title"
|
||||
assert app.screen.query_one("HeaderTitle").text == "new screen title"
|
||||
|
||||
|
||||
async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set():
|
||||
@@ -74,7 +74,7 @@ async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set
|
||||
async with app.run_test() as pilot:
|
||||
app.title = "new app title"
|
||||
await pilot.pause()
|
||||
assert app.query_one("HeaderTitle").text == "screen title"
|
||||
assert app.screen.query_one("HeaderTitle").text == "screen title"
|
||||
|
||||
|
||||
async def test_screen_sub_title_none_is_ignored():
|
||||
@@ -90,7 +90,7 @@ async def test_screen_sub_title_none_is_ignored():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.query_one("HeaderTitle").sub_text == "app sub-title"
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "app sub-title"
|
||||
|
||||
|
||||
async def test_screen_sub_title_overrides_app_sub_title():
|
||||
@@ -108,7 +108,7 @@ async def test_screen_sub_title_overrides_app_sub_title():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.query_one("HeaderTitle").sub_text == "screen sub-title"
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title"
|
||||
|
||||
|
||||
async def test_screen_sub_title_reactive_updates_sub_title():
|
||||
@@ -128,7 +128,7 @@ async def test_screen_sub_title_reactive_updates_sub_title():
|
||||
async with app.run_test() as pilot:
|
||||
app.screen.sub_title = "new screen sub-title"
|
||||
await pilot.pause()
|
||||
assert app.query_one("HeaderTitle").sub_text == "new screen sub-title"
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "new screen sub-title"
|
||||
|
||||
|
||||
async def test_app_sub_title_reactive_does_not_update_sub_title_when_screen_sub_title_is_set():
|
||||
@@ -148,4 +148,4 @@ async def test_app_sub_title_reactive_does_not_update_sub_title_when_screen_sub_
|
||||
async with app.run_test() as pilot:
|
||||
app.sub_title = "new app sub-title"
|
||||
await pilot.pause()
|
||||
assert app.query_one("HeaderTitle").sub_text == "screen sub-title"
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title"
|
||||
|
||||
@@ -126,7 +126,7 @@ async def test_pilot_click_screen():
|
||||
Check we can use `Screen` as a selector for a click."""
|
||||
|
||||
async with App().run_test() as pilot:
|
||||
await pilot.click("Screen")
|
||||
await pilot.click()
|
||||
|
||||
|
||||
async def test_pilot_hover_screen():
|
||||
@@ -135,7 +135,7 @@ async def test_pilot_hover_screen():
|
||||
Check we can use `Screen` as a selector for a hover."""
|
||||
|
||||
async with App().run_test() as pilot:
|
||||
await pilot.hover("Screen")
|
||||
await pilot.hover()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -14,20 +14,20 @@ from textual.widget import Widget
|
||||
from textual.widgets import Input, Label
|
||||
|
||||
|
||||
def test_query_errors():
|
||||
async def test_query_errors():
|
||||
app = App()
|
||||
async with app.run_test():
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query_one("foo_bar")
|
||||
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query_one("foo_bar")
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query("foo_bar")
|
||||
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query("foo_bar")
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query("1")
|
||||
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query("1")
|
||||
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query_one("1")
|
||||
with pytest.raises(InvalidQueryFormat):
|
||||
app.query_one("1")
|
||||
|
||||
|
||||
def test_query():
|
||||
|
||||
@@ -152,7 +152,7 @@ async def test_screen_stack_preserved(ModesApp: Type[App]):
|
||||
# Build the stack up.
|
||||
for _ in range(N):
|
||||
await pilot.press("p")
|
||||
fruits.append(str(app.query_one(Label).renderable))
|
||||
fruits.append(str(app.screen.query_one(Label).renderable))
|
||||
|
||||
assert len(app.screen_stack) == N + 1
|
||||
|
||||
@@ -164,7 +164,7 @@ async def test_screen_stack_preserved(ModesApp: Type[App]):
|
||||
# Check the stack.
|
||||
assert len(app.screen_stack) == N + 1
|
||||
for _ in range(N):
|
||||
assert str(app.query_one(Label).renderable) == fruits.pop()
|
||||
assert str(app.screen.query_one(Label).renderable) == fruits.pop()
|
||||
await pilot.press("o")
|
||||
|
||||
|
||||
|
||||
@@ -20,22 +20,6 @@ skip_py310 = pytest.mark.skipif(
|
||||
)
|
||||
|
||||
|
||||
async def test_screen_walk_children():
|
||||
"""Test query only reports active screen."""
|
||||
|
||||
class ScreensApp(App):
|
||||
pass
|
||||
|
||||
app = ScreensApp()
|
||||
async with app.run_test() as pilot:
|
||||
screen1 = Screen()
|
||||
screen2 = Screen()
|
||||
pilot.app.push_screen(screen1)
|
||||
assert list(pilot.app.query("*")) == [screen1]
|
||||
pilot.app.push_screen(screen2)
|
||||
assert list(pilot.app.query("*")) == [screen2]
|
||||
|
||||
|
||||
async def test_installed_screens():
|
||||
class ScreensApp(App):
|
||||
SCREENS = {
|
||||
|
||||
@@ -341,7 +341,7 @@ async def test_compose_order() -> None:
|
||||
class SelectBugApp(App[None]):
|
||||
async def on_mount(self):
|
||||
await self.push_screen(MyScreen(id="my-screen"))
|
||||
self.query_one(Select)
|
||||
self.screen.query_one(Select)
|
||||
|
||||
app = SelectBugApp()
|
||||
messages: list[Message] = []
|
||||
|
||||
@@ -48,7 +48,7 @@ async def test_escape_key_when_tab_behavior_is_indent():
|
||||
assert isinstance(pilot.app.screen, TextAreaDialog)
|
||||
assert isinstance(pilot.app.focused, TextArea)
|
||||
|
||||
pilot.app.query_one(TextArea).tab_behavior = "indent"
|
||||
pilot.app.screen.query_one(TextArea).tab_behavior = "indent"
|
||||
# Pressing escape should focus the button, not dismiss the dialog screen
|
||||
await pilot.press("escape")
|
||||
assert isinstance(pilot.app.screen, TextAreaDialog)
|
||||
|
||||
Reference in New Issue
Block a user