From 154ada427f8c2f24cdf1763e8645f3126082b79b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Feb 2022 11:19:00 +0000 Subject: [PATCH] Query dom - get child --- sandbox/local_styles.py | 15 +++++++++------ src/textual/app.py | 19 ++++++++++++------- src/textual/css/query.py | 8 +++++--- src/textual/dom.py | 14 ++++++++++++++ src/textual/view.py | 6 ------ tests/test_dom.py | 36 ++++++++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 22 deletions(-) diff --git a/sandbox/local_styles.py b/sandbox/local_styles.py index 8c73aca0f..3f73f89f2 100644 --- a/sandbox/local_styles.py +++ b/sandbox/local_styles.py @@ -1,7 +1,7 @@ -from textual.app import App from textual import events -from textual.widgets import Placeholder +from textual.app import App from textual.widget import Widget +from textual.widgets import Placeholder class BasicApp(App): @@ -20,14 +20,17 @@ class BasicApp(App): await self.dispatch_key(event) def key_a(self) -> None: - self.query("#footer").set_styles(text="on magenta") + footer = self.get_child("#footer") + footer.set_styles(text="on magenta") def key_b(self) -> None: - self["#footer"].set_styles("text: on green") + footer = self.get_child("#footer") + footer.set_styles("text: on green") def key_c(self) -> None: - self["#header"].toggle_class("-highlight") - self.log(self["#header"].styles) + header = self.get_child("#header") + header.toggle_class("-highlight") + self.log(header.styles) BasicApp.run(css_file="local_styles.css", log="textual.log") diff --git a/src/textual/app.py b/src/textual/app.py index f292b1460..b23e111ea 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -36,7 +36,7 @@ from .reactive import Reactive from .view import View from .widget import Widget -from .css.query import EmptyQueryError +from .css.query import NoMatchingNodesError if TYPE_CHECKING: from .css.query import DOMQuery @@ -279,13 +279,18 @@ class App(DOMNode): return DOMQuery(self.view, selector) - def __getitem__(self, selector: str) -> DOMNode: - from .css.query import DOMQuery + def get_child(self, selector: str) -> DOMNode: + """Shorthand for self.view.get_child(selector: str) + Returns the first child (immediate descendent) of this DOMNode + matching the selector. - try: - return DOMQuery(self.view, selector).first() - except EmptyQueryError: - raise KeyError(selector) + Args: + selector (str): A CSS selector. + + Returns: + DOMNode: The first child of this node which matches the selector. + """ + return self.view.get_child(selector) def update_styles(self) -> None: """Request update of styles. diff --git a/src/textual/css/query.py b/src/textual/css/query.py index c7111bd43..9532b34b8 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: from ..dom import DOMNode -class EmptyQueryError(Exception): +class NoMatchingNodesError(Exception): pass @@ -38,7 +38,7 @@ class DOMQuery: selector: str | None = None, nodes: list[DOMNode] | None = None, ) -> None: - + self._selector = selector self._nodes: list[DOMNode] = [] if nodes is not None: self._nodes = nodes @@ -103,7 +103,9 @@ class DOMQuery: if self._nodes: return self._nodes[0] else: - raise EmptyQueryError("Query is empty") + raise NoMatchingNodesError( + f"No nodes match the selector {self._selector!r}" + ) def add_class(self, *class_names: str) -> DOMQuery: """Add the given class name(s) to nodes.""" diff --git a/src/textual/dom.py b/src/textual/dom.py index 3396d81af..c3434567b 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -282,6 +282,20 @@ class DOMNode(MessagePump): if node.children: push(iter(node.children)) + def get_child(self, selector: str) -> DOMNode: + """Return the first child (immediate descendent) of this DOMNode matching a selector. + + Args: + selector (str): A CSS selector. + + Returns: + DOMNode: The first child of this node which matches the selector. + """ + from .css.query import DOMQuery + + query = DOMQuery(selector=selector, nodes=list(self.children)) + return query.first() + def query(self, selector: str | None = None) -> DOMQuery: """Get a DOM query. diff --git a/src/textual/view.py b/src/textual/view.py index 70992b50b..5651fc41e 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -69,12 +69,6 @@ class View(Widget): def __rich_repr__(self) -> rich.repr.Result: yield "name", self.name - def __getitem__(self, widget_id: str) -> Widget: - try: - return self.get_child_by_id(widget_id) - except errors.MissingWidget as error: - raise KeyError(str(error)) - @property def is_visual(self) -> bool: return False diff --git a/tests/test_dom.py b/tests/test_dom.py index cf1c70283..b8eabfcba 100644 --- a/tests/test_dom.py +++ b/tests/test_dom.py @@ -1,6 +1,7 @@ import pytest from textual.css.errors import StyleValueError +from textual.css.query import NoMatchingNodesError from textual.dom import DOMNode @@ -23,3 +24,38 @@ def test_display_set_invalid_value(): node = DOMNode() with pytest.raises(StyleValueError): node.display = "blah" + + +@pytest.fixture +def parent(): + parent = DOMNode(id="parent") + + child1 = DOMNode(id="child1") + child1.add_class("foo") + child2 = DOMNode(id="child2") + child2.add_class("bar") + + grandchild1 = DOMNode(id="grandchild1") + child1.add_child(grandchild1) + + parent.add_child(child1) + parent.add_child(child2) + + yield parent + + +def test_get_child_gets_first_child(parent): + child = parent.get_child(".foo") + assert child.id == "child1" + assert child.get_child("#grandchild1").id == "grandchild1" + assert parent.get_child(".bar").id == "child2" + + +def test_get_child_no_matching_child(parent): + with pytest.raises(NoMatchingNodesError): + parent.get_child("#doesnt-exist") + + +def test_get_child_only_immediate_descendents(parent): + with pytest.raises(NoMatchingNodesError): + parent.get_child("#grandchild1")