Merge branch 'css' into uber-widget

This commit is contained in:
Will McGugan
2022-02-23 10:09:07 +00:00
7 changed files with 77 additions and 26 deletions

View File

@@ -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")

View File

@@ -37,7 +37,7 @@ from .renderables.gradient import VerticalGradient
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
@@ -277,13 +277,18 @@ class App(DOMNode):
return DOMQuery(self.view, selector)
def __getitem__(self, selector: str) -> DOMNode:
from .css.query import DOMQuery
def get_child(self, id: str) -> DOMNode:
"""Shorthand for self.view.get_child(id: str)
Returns the first child (immediate descendent) of this DOMNode
with the given ID.
try:
return DOMQuery(self.view, selector).first()
except EmptyQueryError:
raise KeyError(selector)
Args:
id (str): The ID of the node to search for.
Returns:
DOMNode: The first child of this node with the specified ID.
"""
return self.view.get_child(id)
def render_background(self) -> RenderableType:
gradient = VerticalGradient("red", "blue")

View File

@@ -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."""

View File

@@ -12,8 +12,9 @@ from ._node_list import NodeList
from .css._error_tools import friendly_list
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
from .css.errors import StyleValueError
from .css.styles import Styles, RenderStyles
from .css.parse import parse_declarations
from .css.styles import Styles, RenderStyles
from .css.query import NoMatchingNodesError
from .message_pump import MessagePump
if TYPE_CHECKING:
@@ -282,6 +283,20 @@ class DOMNode(MessagePump):
if node.children:
push(iter(node.children))
def get_child(self, id: str) -> DOMNode:
"""Return the first child (immediate descendent) of this node with the given ID.
Args:
id (str): The ID of the child.
Returns:
DOMNode: The first child of this node with the ID.
"""
for child in self.children:
if child.id == id:
return child
raise NoMatchingNodesError(f"No child found with id={id!r}")
def query(self, selector: str | None = None) -> DOMQuery:
"""Get a DOM query.

View File

@@ -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

View File

@@ -169,7 +169,7 @@ def test_animatable():
assert animate_test.bar.value == 50.0
class TestAnimator(Animator):
class MockAnimator(Animator):
"""A mock animator."""
def __init__(self, *args) -> None:
@@ -187,7 +187,7 @@ class TestAnimator(Animator):
def test_animator():
target = Mock()
animator = TestAnimator(target)
animator = MockAnimator(target)
animate_test = AnimateTest()
# Animate attribute "foo" on animate_test to 100.0 in 10 seconds
@@ -226,7 +226,7 @@ def test_animator():
def test_bound_animator():
target = Mock()
animator = TestAnimator(target)
animator = MockAnimator(target)
animate_test = AnimateTest()
# Bind an animator so it animates attributes on the given object

View File

@@ -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,34 @@ 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")
child2 = DOMNode(id="child2")
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(id="child1")
assert child.id == "child1"
assert child.get_child(id="grandchild1").id == "grandchild1"
assert parent.get_child(id="child2").id == "child2"
def test_get_child_no_matching_child(parent):
with pytest.raises(NoMatchingNodesError):
parent.get_child(id="doesnt-exist")
def test_get_child_only_immediate_descendents(parent):
with pytest.raises(NoMatchingNodesError):
parent.get_child(id="grandchild1")