From 011ac1df5980a8465c87bd77f9e02511d5ed28b7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 14:45:24 +0100 Subject: [PATCH 01/12] Faster query_one --- docs/guide/queries.md | 1 - src/textual/_node_list.py | 21 ++++++--- src/textual/css/model.py | 5 +++ src/textual/dom.py | 89 +++++++++++++++++++++++++++++++++++---- src/textual/widget.py | 11 ++--- tests/test_query.py | 2 +- 6 files changed, 108 insertions(+), 21 deletions(-) diff --git a/docs/guide/queries.md b/docs/guide/queries.md index d33659f382..c0ce0be51f 100644 --- a/docs/guide/queries.md +++ b/docs/guide/queries.md @@ -21,7 +21,6 @@ send_button = self.query_one("#send") This will retrieve a widget with an ID of `send`, if there is exactly one. If there are no matching widgets, Textual will raise a [NoMatches][textual.css.query.NoMatches] exception. -If there is more than one match, Textual will raise a [TooManyMatches][textual.css.query.TooManyMatches] exception. You can also add a second parameter for the expected type, which will ensure that you get the type you are expecting. diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index 198558777d..52555f9d61 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from _typeshed import SupportsRichComparison + from .dom import DOMNode from .widget import Widget @@ -24,7 +25,8 @@ class NodeList(Sequence["Widget"]): Although named a list, widgets may appear only once, making them more like a set. """ - def __init__(self) -> None: + def __init__(self, parent: DOMNode | None = None) -> None: + self._parent = parent # The nodes in the list self._nodes: list[Widget] = [] self._nodes_set: set[Widget] = set() @@ -52,6 +54,13 @@ def __len__(self) -> int: def __contains__(self, widget: object) -> bool: return widget in self._nodes + def updated(self) -> None: + """Mark the nodes as having been updated.""" + self._updates += 1 + node = self._parent + while node is not None and (node := node._parent) is not None: + node._nodes._updates += 1 + def _sort( self, *, @@ -69,7 +78,7 @@ def _sort( else: self._nodes.sort(key=key, reverse=reverse) - self._updates += 1 + self.updated() def index(self, widget: Any, start: int = 0, stop: int = sys.maxsize) -> int: """Return the index of the given widget. @@ -102,7 +111,7 @@ def _append(self, widget: Widget) -> None: if widget_id is not None: self._ensure_unique_id(widget_id) self._nodes_by_id[widget_id] = widget - self._updates += 1 + self.updated() def _insert(self, index: int, widget: Widget) -> None: """Insert a Widget. @@ -117,7 +126,7 @@ def _insert(self, index: int, widget: Widget) -> None: if widget_id is not None: self._ensure_unique_id(widget_id) self._nodes_by_id[widget_id] = widget - self._updates += 1 + self.updated() def _ensure_unique_id(self, widget_id: str) -> None: if widget_id in self._nodes_by_id: @@ -141,7 +150,7 @@ def _remove(self, widget: Widget) -> None: widget_id = widget.id if widget_id in self._nodes_by_id: del self._nodes_by_id[widget_id] - self._updates += 1 + self.updated() def _clear(self) -> None: """Clear the node list.""" @@ -149,7 +158,7 @@ def _clear(self) -> None: self._nodes.clear() self._nodes_set.clear() self._nodes_by_id.clear() - self._updates += 1 + self.updated() def __iter__(self) -> Iterator[Widget]: return iter(self._nodes) diff --git a/src/textual/css/model.py b/src/textual/css/model.py index b2bf25f9a8..a7a044f120 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -193,6 +193,11 @@ def __post_init__(self) -> None: def css(self) -> str: return RuleSet._selector_to_css(self.selectors) + @property + def has_pseudo_selectors(self) -> bool: + """Are there any pseudo selectors in the SelectorSet?""" + return any(selector.pseudo_classes for selector in self.selectors) + def __rich_repr__(self) -> rich.repr.Result: selectors = RuleSet._selector_to_css(self.selectors) yield selectors diff --git a/src/textual/dom.py b/src/textual/dom.py index b75bbda542..7aec2088f8 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -37,7 +37,9 @@ from .css._error_tools import friendly_list from .css.constants import VALID_DISPLAY, VALID_VISIBILITY from .css.errors import DeclarationError, StyleValueError -from .css.parse import parse_declarations +from .css.match import match +from .css.parse import parse_declarations, parse_selectors +from .css.query import NoMatches, TooManyMatches from .css.styles import RenderStyles, Styles from .css.tokenize import IDENTIFIER from .message_pump import MessagePump @@ -60,7 +62,7 @@ from .worker import Worker, WorkType, ResultType # Unused & ignored imports are needed for the docs to link to these objects: - from .css.query import NoMatches, TooManyMatches, WrongType # type: ignore # noqa: F401 + from .css.query import WrongType # type: ignore # noqa: F401 from typing_extensions import Literal @@ -184,13 +186,14 @@ def __init__( self._name = name self._id = None if id is not None: - self.id = id + check_identifiers("id", id) + self._id = id _classes = classes.split() if classes else [] check_identifiers("class name", *_classes) self._classes.update(_classes) - self._nodes: NodeList = NodeList() + self._nodes: NodeList = NodeList(self) self._css_styles: Styles = Styles(self) self._inline_styles: Styles = Styles(self) self.styles: RenderStyles = RenderStyles( @@ -213,6 +216,7 @@ def __init__( dict[str, tuple[MessagePump, Reactive | object]] | None ) = None self._pruning = False + super().__init__() def set_reactive( @@ -1393,21 +1397,90 @@ def query_one( Raises: WrongType: If the wrong type was found. NoMatches: If no node matches the query. - TooManyMatches: If there is more than one matching node in the query. Returns: A widget matching the selector. """ _rich_traceback_omit = True - from .css.query import DOMQuery if isinstance(selector, str): query_selector = selector else: query_selector = selector.__name__ - query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector) - return query.only_one() if expect_type is None else query.only_one(expect_type) + selector_set = parse_selectors(query_selector) + + children = walk_depth_first(self) + iter_children = iter(children) + for node in iter_children: + if not match(selector_set, node): + continue + if expect_type is not None and not isinstance(node, expect_type): + continue + return node + + raise NoMatches(f"No nodes match {selector!r} on {self!r}") + + if TYPE_CHECKING: + + @overload + def query_exactly_one(self, selector: str) -> Widget: ... + + @overload + def query_exactly_one(self, selector: type[QueryType]) -> QueryType: ... + + @overload + def query_exactly_one( + self, selector: str, expect_type: type[QueryType] + ) -> QueryType: ... + + def query_exactly_one( + self, + selector: str | type[QueryType], + expect_type: type[QueryType] | None = None, + ) -> QueryType | Widget: + """Get a widget from this widget's children that matches a selector or widget type. + + !!! Note + This method is similar to [query_one][textual.dom.DOMNode.query_one]. + The only difference is that it will raise `TooManyMatches` if there is more than a single match. + + Args: + selector: A selector or widget type. + expect_type: Require the object be of the supplied type, or None for any type. + + Raises: + WrongType: If the wrong type was found. + NoMatches: If no node matches the query. + TooManyMatches: If there is more than one matching node in the query (and `exactly_one==True`). + + Returns: + A widget matching the selector. + """ + _rich_traceback_omit = True + + if isinstance(selector, str): + query_selector = selector + else: + query_selector = selector.__name__ + + selector_set = parse_selectors(query_selector) + + children = walk_depth_first(self) + iter_children = iter(children) + for node in iter_children: + if not match(selector_set, node): + continue + if expect_type is not None and not isinstance(node, expect_type): + continue + for later_node in iter_children: + if match(selector_set, later_node): + raise TooManyMatches( + "Call to query_one resulted in more than one matched node" + ) + return node + + raise NoMatches(f"No nodes match {selector!r} on {self!r}") def set_styles(self, css: str | None = None, **update_styles: Any) -> Self: """Set custom styles on this object. diff --git a/src/textual/widget.py b/src/textual/widget.py index 9296d20865..a70e524e55 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -810,10 +810,11 @@ def get_widget_by_id( # We use Widget as a filter_type so that the inferred type of child is Widget. for child in walk_depth_first(self, filter_type=Widget): try: - if expect_type is None: - return child.get_child_by_id(id) - else: - return child.get_child_by_id(id, expect_type=expect_type) + if child._nodes: + if expect_type is None: + return child.get_child_by_id(id) + else: + return child.get_child_by_id(id, expect_type=expect_type) except NoMatches: pass except WrongType as exc: @@ -958,7 +959,7 @@ def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]: # can be passed to query_one. So let's use that to get a widget to # work on. if isinstance(spot, str): - spot = self.query_one(spot, Widget) + spot = self.query_exactly_one(spot, Widget) # At this point we should have a widget, either because we got given # one, or because we pulled one out of the query. First off, does it diff --git a/tests/test_query.py b/tests/test_query.py index 07f608824a..4030003866 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -103,7 +103,7 @@ class App(Widget): assert app.query_one("#widget1") == widget1 assert app.query_one("#widget1", Widget) == widget1 with pytest.raises(TooManyMatches): - _ = app.query_one(Widget) + _ = app.query_exactly_one(Widget) assert app.query("Widget.float")[0] == sidebar assert app.query("Widget.float")[0:2] == [sidebar, tooltip] From b3a4f2a93e412357be33fbbaf981c636efd6e1bf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 14:49:52 +0100 Subject: [PATCH 02/12] changelog --- CHANGELOG.md | 2 ++ src/textual/dom.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b67fb2d3cb..65893ad824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Added `DOMNode.check_consume_key` https://github.com/Textualize/textual/pull/4940 +- Added `DOMNode.query_exactly_one` ### Changed - KeyPanel will show multiple keys if bound to the same action https://github.com/Textualize/textual/pull/4940 +- `DOMNode.query_one` will not `raise TooManyMatches` ## [0.78.0] - 2024-08-27 diff --git a/src/textual/dom.py b/src/textual/dom.py index 7aec2088f8..7ccedcf162 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1410,9 +1410,7 @@ def query_one( selector_set = parse_selectors(query_selector) - children = walk_depth_first(self) - iter_children = iter(children) - for node in iter_children: + for node in walk_depth_first(self): if not match(selector_set, node): continue if expect_type is not None and not isinstance(node, expect_type): @@ -1475,6 +1473,8 @@ def query_exactly_one( continue for later_node in iter_children: if match(selector_set, later_node): + if expect_type is not None and not isinstance(node, expect_type): + continue raise TooManyMatches( "Call to query_one resulted in more than one matched node" ) From a7ce51d0115fe02a4a6a8db94760d049e9f75562 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:05:17 +0100 Subject: [PATCH 03/12] added caching of query_one --- src/textual/css/model.py | 6 ++++++ src/textual/dom.py | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/textual/css/model.py b/src/textual/css/model.py index a7a044f120..08e626ca1e 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -198,6 +198,12 @@ def has_pseudo_selectors(self) -> bool: """Are there any pseudo selectors in the SelectorSet?""" return any(selector.pseudo_classes for selector in self.selectors) + @property + def is_simple(self) -> bool: + """Are all the selectors simple (i.e. only dependent on static DOM state).""" + simple_types = {SelectorType.ID, SelectorType.TYPE} + return all(selector.type in simple_types for selector in self.selectors) + def __rich_repr__(self) -> rich.repr.Result: selectors = RuleSet._selector_to_css(self.selectors) yield selectors diff --git a/src/textual/dom.py b/src/textual/dom.py index 7ccedcf162..22f18a9a86 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -29,6 +29,8 @@ from rich.text import Text from rich.tree import Tree +from textual.cache import LRUCache + from ._context import NoActiveAppError, active_message_pump from ._node_list import NodeList from ._types import WatchCallbackType @@ -216,6 +218,7 @@ def __init__( dict[str, tuple[MessagePump, Reactive | object]] | None ) = None self._pruning = False + self._query_one_cache: LRUCache[tuple[object, ...], DOMNode] = LRUCache(1024) super().__init__() @@ -745,7 +748,7 @@ def id(self, new_id: str) -> str: ValueError: If the ID has already been set. """ check_identifiers("id", new_id) - + self._nodes.update() if self._id is not None: raise ValueError( f"Node 'id' attribute may not be changed once set (current id={self._id!r})" @@ -1410,11 +1413,21 @@ def query_one( selector_set = parse_selectors(query_selector) + 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) + if cached_result is not None: + return cached_result + else: + cache_key = None + for node in walk_depth_first(self): 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 return node raise NoMatches(f"No nodes match {selector!r} on {self!r}") @@ -1464,6 +1477,14 @@ def query_exactly_one( selector_set = parse_selectors(query_selector) + 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) + if cached_result is not None: + return cached_result + else: + cache_key = None + children = walk_depth_first(self) iter_children = iter(children) for node in iter_children: @@ -1478,6 +1499,8 @@ def query_exactly_one( raise TooManyMatches( "Call to query_one resulted in more than one matched node" ) + if cache_key is not None: + self._query_one_cache[cache_key] = node return node raise NoMatches(f"No nodes match {selector!r} on {self!r}") From eaf4dbeac8a2ffa74e6e006f8295a61efe1640bc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:12:14 +0100 Subject: [PATCH 04/12] superfluous --- src/textual/css/model.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/textual/css/model.py b/src/textual/css/model.py index 08e626ca1e..c75f4bb74c 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -193,16 +193,15 @@ def __post_init__(self) -> None: def css(self) -> str: return RuleSet._selector_to_css(self.selectors) - @property - def has_pseudo_selectors(self) -> bool: - """Are there any pseudo selectors in the SelectorSet?""" - return any(selector.pseudo_classes for selector in self.selectors) - @property def is_simple(self) -> bool: """Are all the selectors simple (i.e. only dependent on static DOM state).""" simple_types = {SelectorType.ID, SelectorType.TYPE} - return all(selector.type in simple_types for selector in self.selectors) + return all( + selector.type in simple_types + for selector in self.selectors + if not selector.pseudo_classes + ) def __rich_repr__(self) -> rich.repr.Result: selectors = RuleSet._selector_to_css(self.selectors) From fdbeaf4d4fadfc9d2250c94f47d0085c6c2a9808 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:13:18 +0100 Subject: [PATCH 05/12] changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65893ad824..91d47e3543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Added `DOMNode.check_consume_key` https://github.com/Textualize/textual/pull/4940 -- Added `DOMNode.query_exactly_one` +- Added `DOMNode.query_exactly_one` https://github.com/Textualize/textual/pull/4950 +- Added `SelectorSet.is_simple` https://github.com/Textualize/textual/pull/4950 ### Changed - KeyPanel will show multiple keys if bound to the same action https://github.com/Textualize/textual/pull/4940 -- `DOMNode.query_one` will not `raise TooManyMatches` +- `DOMNode.query_one` will not `raise TooManyMatches` https://github.com/Textualize/textual/pull/4950 ## [0.78.0] - 2024-08-27 From ac18a7f312bbeb3e6e2c9358a9b94d94b5497cd1 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:16:41 +0100 Subject: [PATCH 06/12] fix updated method call --- src/textual/dom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 22f18a9a86..659ce0ca4d 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -748,7 +748,7 @@ def id(self, new_id: str) -> str: ValueError: If the ID has already been set. """ check_identifiers("id", new_id) - self._nodes.update() + self._nodes.updated() if self._id is not None: raise ValueError( f"Node 'id' attribute may not be changed once set (current id={self._id!r})" From 0b9d768ea059b13cc9d381df40e6f6015615c704 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:21:40 +0100 Subject: [PATCH 07/12] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d47e3543..c7eaa07440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - KeyPanel will show multiple keys if bound to the same action https://github.com/Textualize/textual/pull/4940 -- `DOMNode.query_one` will not `raise TooManyMatches` https://github.com/Textualize/textual/pull/4950 +- Breaking change: `DOMNode.query_one` will not `raise TooManyMatches` https://github.com/Textualize/textual/pull/4950 ## [0.78.0] - 2024-08-27 From 2088c663a4d2cfdf7b45cbfa6695a62ad3774f0b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:23:04 +0100 Subject: [PATCH 08/12] remove bad optimization --- src/textual/widget.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index a70e524e55..ed0f3ec87c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -810,11 +810,10 @@ def get_widget_by_id( # We use Widget as a filter_type so that the inferred type of child is Widget. for child in walk_depth_first(self, filter_type=Widget): try: - if child._nodes: - if expect_type is None: - return child.get_child_by_id(id) - else: - return child.get_child_by_id(id, expect_type=expect_type) + if expect_type is None: + return child.get_child_by_id(id) + else: + return child.get_child_by_id(id, expect_type=expect_type) except NoMatches: pass except WrongType as exc: From 3beef2ae16174015baddcfa619709d430b2a86cd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:37:07 +0100 Subject: [PATCH 09/12] optimize get_widget_by_id --- src/textual/dom.py | 4 ++-- src/textual/widget.py | 24 ++++++++---------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 659ce0ca4d..1cd4c63e31 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1421,7 +1421,7 @@ def query_one( else: cache_key = None - for node in walk_depth_first(self): + for node in walk_depth_first(self, with_root=False): if not match(selector_set, node): continue if expect_type is not None and not isinstance(node, expect_type): @@ -1485,7 +1485,7 @@ def query_exactly_one( else: cache_key = None - children = walk_depth_first(self) + children = walk_depth_first(self, with_root=False) iter_children = iter(children) for node in iter_children: if not match(selector_set, node): diff --git a/src/textual/widget.py b/src/textual/widget.py index ed0f3ec87c..b71cc971ae 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -87,7 +87,6 @@ from .renderables.blank import Blank from .rlock import RLock from .strip import Strip -from .walk import walk_depth_first if TYPE_CHECKING: from .app import App, ComposeResult @@ -807,21 +806,14 @@ def get_widget_by_id( NoMatches: if no children could be found for this ID. WrongType: if the wrong type was found. """ - # We use Widget as a filter_type so that the inferred type of child is Widget. - for child in walk_depth_first(self, filter_type=Widget): - try: - if expect_type is None: - return child.get_child_by_id(id) - else: - 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}") + + widget = self.query_one(f"#{id}") + if expect_type is not None and not isinstance(widget, expect_type): + raise WrongType( + f"Descendant with id={id!r} is wrong type; expected {expect_type}," + f" got {type(widget)}" + ) + return widget def get_child_by_type(self, expect_type: type[ExpectType]) -> ExpectType: """Get the first immediate child of a given type. From a709193e91e2a453f32e9b5d619fa46a4a063e99 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:49:30 +0100 Subject: [PATCH 10/12] cache key type alias fix is_simple --- src/textual/css/model.py | 3 +-- src/textual/dom.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/textual/css/model.py b/src/textual/css/model.py index c75f4bb74c..cf5f55b83b 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -198,9 +198,8 @@ def is_simple(self) -> bool: """Are all the selectors simple (i.e. only dependent on static DOM state).""" simple_types = {SelectorType.ID, SelectorType.TYPE} return all( - selector.type in simple_types + (selector.type in simple_types and not selector.pseudo_classes) for selector in self.selectors - if not selector.pseudo_classes ) def __rich_repr__(self) -> rich.repr.Result: diff --git a/src/textual/dom.py b/src/textual/dom.py index 1cd4c63e31..361c8528a2 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -78,6 +78,10 @@ ReactiveType = TypeVar("ReactiveType") +QueryOneCacheKey: TypeAlias = "tuple[int, str, Type[Widget]]" +"""The key used to cache query_one results.""" + + class BadIdentifier(Exception): """Exception raised if you supply a `id` attribute or class name in the wrong format.""" @@ -218,7 +222,7 @@ def __init__( dict[str, tuple[MessagePump, Reactive | object]] | None ) = None self._pruning = False - self._query_one_cache: LRUCache[tuple[object, ...], DOMNode] = LRUCache(1024) + self._query_one_cache: LRUCache[QueryOneCacheKey, DOMNode] = LRUCache(1024) super().__init__() From 95c8204ce00a98aaade88a97edb6129a2a4d7f26 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:50:39 +0100 Subject: [PATCH 11/12] fix cache key --- src/textual/dom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 361c8528a2..6db5b4a1dc 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -78,7 +78,7 @@ ReactiveType = TypeVar("ReactiveType") -QueryOneCacheKey: TypeAlias = "tuple[int, str, Type[Widget]]" +QueryOneCacheKey: TypeAlias = "tuple[int, str, Type[Widget] | None]" """The key used to cache query_one results.""" From 78bd0f509a3863799d1a609988594651d1e3c432 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 28 Aug 2024 16:55:43 +0100 Subject: [PATCH 12/12] import --- src/textual/dom.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 6db5b4a1dc..0fc190e55a 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -29,12 +29,11 @@ from rich.text import Text from rich.tree import Tree -from textual.cache import LRUCache - from ._context import NoActiveAppError, active_message_pump from ._node_list import NodeList from ._types import WatchCallbackType from .binding import Binding, BindingsMap, BindingType +from .cache import LRUCache from .color import BLACK, WHITE, Color from .css._error_tools import friendly_list from .css.constants import VALID_DISPLAY, VALID_VISIBILITY