From e18fac9033ec8c86417e4c7b7963c4b6184db6a9 Mon Sep 17 00:00:00 2001 From: Aaron Stephens Date: Sun, 20 Nov 2022 16:35:30 -0800 Subject: [PATCH 1/3] feat: get_*_by_id expect_type --- src/textual/_node_list.py | 11 ++++--- src/textual/app.py | 52 ++++++++++++++++++++++++++----- src/textual/widget.py | 64 ++++++++++++++++++++++++++++++++------- 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index d48597090..966317f6d 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -63,8 +63,9 @@ class NodeList(Sequence): """ return self._nodes.index(widget) - def _get_by_id(self, widget_id: str) -> Widget | None: - """Get the widget for the given widget_id, or None if there's no matches in this list""" + def get_by_id(self, widget_id: str) -> Widget | None: + """Get the widget for the given widget_id, or None if there's no matches in this list + """ return self._nodes_by_id.get(widget_id) def _append(self, widget: Widget) -> None: @@ -100,9 +101,9 @@ class NodeList(Sequence): def _ensure_unique_id(self, widget_id: str) -> None: if widget_id in self._nodes_by_id: raise DuplicateIds( - f"Tried to insert a widget with ID {widget_id!r}, but a widget {self._nodes_by_id[widget_id]!r} " - f"already exists with that ID in this list of children. " - f"The children of a widget must have unique IDs." + f"Tried to insert a widget with ID {widget_id!r}, but a widget" + f" {self._nodes_by_id[widget_id]!r} already exists with that ID in this" + " list of children. The children of a widget must have unique IDs." ) def _remove(self, widget: Widget) -> None: diff --git a/src/textual/app.py b/src/textual/app.py index 27fbc352a..005c05470 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -26,6 +26,7 @@ from typing import ( TypeVar, Union, cast, + overload, ) from weakref import WeakSet, WeakValueDictionary @@ -877,7 +878,8 @@ class App(Generic[ReturnType], DOMNode): stylesheet.parse() elapsed = (perf_counter() - time) * 1000 self.log.system( - f" loaded {len(css_paths)} CSS files in {elapsed:.0f} ms" + f" loaded {len(css_paths)} CSS files in" + f" {elapsed:.0f} ms" ) except Exception as error: # TODO: Catch specific exceptions @@ -892,23 +894,52 @@ class App(Generic[ReturnType], DOMNode): def render(self) -> RenderableType: return Blank(self.styles.background) + ExpectType = TypeVar("ExpectType", bound=Widget) + + @overload def get_child_by_id(self, id: str) -> Widget: + ... + + @overload + def get_child_by_id(self, id: str, expect_type: type[ExpectType]) -> ExpectType: + ... + + def get_child_by_id( + self, id: str, expect_type: type[ExpectType] | None = None + ) -> ExpectType | Widget: """Shorthand for self.screen.get_child(id: str) Returns the first child (immediate descendent) of this DOMNode with the given ID. Args: id (str): The ID of the node to search for. + expect_type (type | None, optional): Require the object be of the supplied type, or None for any type. + Defaults to None. Returns: - DOMNode: The first child of this node with the specified ID. + ExpectType | Widget: The first child of this node with the specified ID. Raises: NoMatches: if no children could be found for this ID + WrongType: if the wrong type was found. """ - return self.screen.get_child_by_id(id) + return ( + self.screen.get_child_by_id(id) + if expect_type is None + else self.screen.get_child_by_id(id, expect_type) + ) + @overload def get_widget_by_id(self, id: str) -> Widget: + ... + + @overload + def get_widget_by_id(self, id: str, expect_type: type[ExpectType]) -> ExpectType: + ... + + def get_widget_by_id( + self, id: str, expect_type: type[ExpectType] | None = None + ) -> ExpectType | Widget: """Shorthand for self.screen.get_widget_by_id(id) Return the first descendant widget with the given ID. @@ -918,14 +949,21 @@ class App(Generic[ReturnType], DOMNode): Args: id (str): The ID to search for in the subtree + expect_type (type | None, optional): Require the object be of the supplied type, or None for any type. + Defaults to None. Returns: - DOMNode: The first descendant encountered with this ID. + ExpectType | Widget: The first descendant encountered with this ID. Raises: NoMatches: if no children could be found for this ID + WrongType: if the wrong type was found. """ - return self.screen.get_widget_by_id(id) + return ( + self.screen.get_widget_by_id(id) + if expect_type is None + else self.screen.get_widget_by_id(id, expect_type) + ) def update_styles(self, node: DOMNode | None = None) -> None: """Request update of styles. @@ -1463,7 +1501,6 @@ class App(Generic[ReturnType], DOMNode): # If we don't already know about this widget... if child not in self._registry: - # Now to figure out where to place it. If we've got a `before`... if before is not None: # ...it's safe to NodeList._insert before that location. @@ -1792,7 +1829,8 @@ class App(Generic[ReturnType], DOMNode): if private_method is None and public_method is None: log( - f" {action_name!r} has no target. Couldn't find methods {public_method_name!r} or {private_method_name!r}" + f" {action_name!r} has no target. Couldn't find methods" + f" {public_method_name!r} or {private_method_name!r}" ) if callable(private_method): diff --git a/src/textual/widget.py b/src/textual/widget.py index fcc8aa328..cb0311982 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -13,7 +13,9 @@ from typing import ( Iterable, NamedTuple, Sequence, + TypeVar, cast, + overload, ) import rich.repr @@ -42,7 +44,7 @@ from ._styles_cache import StylesCache from ._types import Lines from .binding import NoBinding from .box_model import BoxModel, get_box_model -from .css.query import NoMatches +from .css.query import NoMatches, WrongType from .css.scalar import ScalarOffset from .dom import DOMNode, NoScreen from .geometry import Offset, Region, Size, Spacing, clamp @@ -209,7 +211,6 @@ class Widget(DOMNode): id: str | None = None, classes: str | None = None, ) -> None: - self._size = Size(0, 0) self._container_size = Size(0, 0) self._layout_required = False @@ -337,41 +338,81 @@ class Widget(DOMNode): def offset(self, offset: Offset) -> None: self.styles.offset = ScalarOffset.from_offset(offset) + ExpectType = TypeVar("ExpectType", bound="Widget") + + @overload def get_child_by_id(self, id: str) -> Widget: + ... + + @overload + def get_child_by_id(self, id: str, expect_type: type[ExpectType]) -> ExpectType: + ... + + def get_child_by_id( + self, id: str, expect_type: type[ExpectType] | None = None + ) -> ExpectType | Widget: """Return the first child (immediate descendent) of this node with the given ID. Args: id (str): The ID of the child. + expect_type (type | None, optional): Require the object be of the supplied type, or None for any type. + Defaults to None. Returns: - DOMNode: The first child of this node with the ID. + ExpectType | Widget: The first child of this node with the ID. Raises: NoMatches: if no children could be found for this ID + WrongType: if the wrong type was found. """ - child = self.children._get_by_id(id) - if child is not None: + child = self.children.get_by_id(id) + if child is None: + raise NoMatches(f"No child found with id={id!r}") + if expect_type is None: return child - raise NoMatches(f"No child found with id={id!r}") + if not isinstance(child, expect_type): + raise WrongType( + f"Child with id={id!r} is wrong type; expected {expect_type}, got" + f" {type(child)}" + ) + return child + @overload def get_widget_by_id(self, id: str) -> Widget: + ... + + @overload + def get_widget_by_id(self, id: str, expect_type: type[ExpectType]) -> ExpectType: + ... + + def get_widget_by_id( + self, id: str, expect_type: type[ExpectType] | None = None + ) -> ExpectType | Widget: """Return the first descendant widget with the given ID. Performs a depth-first search rooted at this widget. Args: id (str): The ID to search for in the subtree + expect_type (type | None, optional): Require the object be of the supplied type, or None for any type. + Defaults to None. Returns: - DOMNode: The first descendant encountered with this ID. + ExpectType | Widget: The first descendant encountered with this ID. Raises: NoMatches: if no children could be found for this ID + WrongType: if the wrong type was found. """ for child in walk_depth_first(self): try: - return child.get_child_by_id(id) + return child.get_child_by_id(id, expect_type=expect_type) except NoMatches: pass + except WrongType as exc: + raise WrongType( + f"Descendant with id={id!r} is wrong type; expected {expect_type}," + f" got {type(child)}" + ) from exc raise NoMatches(f"No descendant found with id={id!r}") def get_component_rich_style(self, name: str) -> Style: @@ -511,8 +552,8 @@ class Widget(DOMNode): for widget_id, count in counter.items(): if count > 1: raise MountError( - f"Tried to insert {count!r} widgets with the same ID {widget_id!r}. " - f"Widget IDs must be unique." + f"Tried to insert {count!r} widgets with the same ID" + f" {widget_id!r}. Widget IDs must be unique." ) # Saying you want to mount before *and* after something is an error. @@ -573,7 +614,8 @@ class Widget(DOMNode): child = self.children[child] except IndexError: raise WidgetError( - f"An index of {child} for the child to {called} is out of bounds" + f"An index of {child} for the child to {called} is out of" + " bounds" ) from None else: # We got an actual widget, so let's be sure it really is one of From 710c69d3af84a6defd4810b39b57afe62746f03e Mon Sep 17 00:00:00 2001 From: Aaron Stephens Date: Sun, 20 Nov 2022 16:58:49 -0800 Subject: [PATCH 2/3] fix: formatting --- src/textual/_node_list.py | 9 ++++----- src/textual/app.py | 6 ++---- src/textual/widget.py | 7 +++---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index 966317f6d..a398373fc 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -64,8 +64,7 @@ class NodeList(Sequence): return self._nodes.index(widget) def get_by_id(self, widget_id: str) -> Widget | None: - """Get the widget for the given widget_id, or None if there's no matches in this list - """ + """Get the widget for the given widget_id, or None if there's no matches in this list""" return self._nodes_by_id.get(widget_id) def _append(self, widget: Widget) -> None: @@ -101,9 +100,9 @@ class NodeList(Sequence): def _ensure_unique_id(self, widget_id: str) -> None: if widget_id in self._nodes_by_id: raise DuplicateIds( - f"Tried to insert a widget with ID {widget_id!r}, but a widget" - f" {self._nodes_by_id[widget_id]!r} already exists with that ID in this" - " list of children. The children of a widget must have unique IDs." + f"Tried to insert a widget with ID {widget_id!r}, but a widget {self._nodes_by_id[widget_id]!r} " + "already exists with that ID in this list of children. " + "The children of a widget must have unique IDs." ) def _remove(self, widget: Widget) -> None: diff --git a/src/textual/app.py b/src/textual/app.py index 005c05470..369583427 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -878,8 +878,7 @@ class App(Generic[ReturnType], DOMNode): stylesheet.parse() elapsed = (perf_counter() - time) * 1000 self.log.system( - f" loaded {len(css_paths)} CSS files in" - f" {elapsed:.0f} ms" + f" loaded {len(css_paths)} CSS files in {elapsed:.0f} ms" ) except Exception as error: # TODO: Catch specific exceptions @@ -1829,8 +1828,7 @@ class App(Generic[ReturnType], DOMNode): if private_method is None and public_method is None: log( - f" {action_name!r} has no target. Couldn't find methods" - f" {public_method_name!r} or {private_method_name!r}" + f" {action_name!r} has no target. Couldn't find methods {public_method_name!r} or {private_method_name!r}" ) if callable(private_method): diff --git a/src/textual/widget.py b/src/textual/widget.py index cb0311982..4847e941e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -552,8 +552,8 @@ class Widget(DOMNode): for widget_id, count in counter.items(): if count > 1: raise MountError( - f"Tried to insert {count!r} widgets with the same ID" - f" {widget_id!r}. Widget IDs must be unique." + f"Tried to insert {count!r} widgets with the same ID {widget_id!r}. " + "Widget IDs must be unique." ) # Saying you want to mount before *and* after something is an error. @@ -614,8 +614,7 @@ class Widget(DOMNode): child = self.children[child] except IndexError: raise WidgetError( - f"An index of {child} for the child to {called} is out of" - " bounds" + f"An index of {child} for the child to {called} is out of bounds" ) from None else: # We got an actual widget, so let's be sure it really is one of From 04246aebbb42f22f10fb3840e14a58acfd04e48d Mon Sep 17 00:00:00 2001 From: Aaron Stephens Date: Thu, 24 Nov 2022 22:36:25 -0800 Subject: [PATCH 3/3] fix: revert _get_by_id to private --- src/textual/_node_list.py | 2 +- src/textual/widget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index a398373fc..7032b0cc7 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -63,7 +63,7 @@ class NodeList(Sequence): """ return self._nodes.index(widget) - def get_by_id(self, widget_id: str) -> Widget | None: + def _get_by_id(self, widget_id: str) -> Widget | None: """Get the widget for the given widget_id, or None if there's no matches in this list""" return self._nodes_by_id.get(widget_id) diff --git a/src/textual/widget.py b/src/textual/widget.py index 4847e941e..d07bc6d67 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -365,7 +365,7 @@ class Widget(DOMNode): NoMatches: if no children could be found for this ID WrongType: if the wrong type was found. """ - child = self.children.get_by_id(id) + child = self.children._get_by_id(id) if child is None: raise NoMatches(f"No child found with id={id!r}") if expect_type is None: