3333from ._node_list import NodeList
3434from ._types import WatchCallbackType
3535from .binding import Binding , BindingsMap , BindingType
36+ from .cache import LRUCache
3637from .color import BLACK , WHITE , Color
3738from .css ._error_tools import friendly_list
3839from .css .constants import VALID_DISPLAY , VALID_VISIBILITY
3940from .css .errors import DeclarationError , StyleValueError
40- from .css .parse import parse_declarations
41+ from .css .match import match
42+ from .css .parse import parse_declarations , parse_selectors
43+ from .css .query import NoMatches , TooManyMatches
4144from .css .styles import RenderStyles , Styles
4245from .css .tokenize import IDENTIFIER
4346from .message_pump import MessagePump
6063 from .worker import Worker , WorkType , ResultType
6164
6265 # Unused & ignored imports are needed for the docs to link to these objects:
63- from .css .query import NoMatches , TooManyMatches , WrongType # type: ignore # noqa: F401
66+ from .css .query import WrongType # type: ignore # noqa: F401
6467
6568from typing_extensions import Literal
6669
7477ReactiveType = TypeVar ("ReactiveType" )
7578
7679
80+ QueryOneCacheKey : TypeAlias = "tuple[int, str, Type[Widget] | None]"
81+ """The key used to cache query_one results."""
82+
83+
7784class BadIdentifier (Exception ):
7885 """Exception raised if you supply a `id` attribute or class name in the wrong format."""
7986
@@ -184,13 +191,14 @@ def __init__(
184191 self ._name = name
185192 self ._id = None
186193 if id is not None :
187- self .id = id
194+ check_identifiers ("id" , id )
195+ self ._id = id
188196
189197 _classes = classes .split () if classes else []
190198 check_identifiers ("class name" , * _classes )
191199 self ._classes .update (_classes )
192200
193- self ._nodes : NodeList = NodeList ()
201+ self ._nodes : NodeList = NodeList (self )
194202 self ._css_styles : Styles = Styles (self )
195203 self ._inline_styles : Styles = Styles (self )
196204 self .styles : RenderStyles = RenderStyles (
@@ -213,6 +221,8 @@ def __init__(
213221 dict [str , tuple [MessagePump , Reactive | object ]] | None
214222 ) = None
215223 self ._pruning = False
224+ self ._query_one_cache : LRUCache [QueryOneCacheKey , DOMNode ] = LRUCache (1024 )
225+
216226 super ().__init__ ()
217227
218228 def set_reactive (
@@ -741,7 +751,7 @@ def id(self, new_id: str) -> str:
741751 ValueError: If the ID has already been set.
742752 """
743753 check_identifiers ("id" , new_id )
744-
754+ self . _nodes . updated ()
745755 if self ._id is not None :
746756 raise ValueError (
747757 f"Node 'id' attribute may not be changed once set (current id={ self ._id !r} )"
@@ -1393,21 +1403,110 @@ def query_one(
13931403 Raises:
13941404 WrongType: If the wrong type was found.
13951405 NoMatches: If no node matches the query.
1396- TooManyMatches: If there is more than one matching node in the query.
13971406
13981407 Returns:
13991408 A widget matching the selector.
14001409 """
14011410 _rich_traceback_omit = True
1402- from .css .query import DOMQuery
14031411
14041412 if isinstance (selector , str ):
14051413 query_selector = selector
14061414 else :
14071415 query_selector = selector .__name__
1408- query : DOMQuery [Widget ] = DOMQuery (self , filter = query_selector )
14091416
1410- return query .only_one () if expect_type is None else query .only_one (expect_type )
1417+ selector_set = parse_selectors (query_selector )
1418+
1419+ if all (selectors .is_simple for selectors in selector_set ):
1420+ cache_key = (self ._nodes ._updates , query_selector , expect_type )
1421+ cached_result = self ._query_one_cache .get (cache_key )
1422+ if cached_result is not None :
1423+ return cached_result
1424+ else :
1425+ cache_key = None
1426+
1427+ for node in walk_depth_first (self , with_root = False ):
1428+ if not match (selector_set , node ):
1429+ continue
1430+ if expect_type is not None and not isinstance (node , expect_type ):
1431+ continue
1432+ if cache_key is not None :
1433+ self ._query_one_cache [cache_key ] = node
1434+ return node
1435+
1436+ raise NoMatches (f"No nodes match { selector !r} on { self !r} " )
1437+
1438+ if TYPE_CHECKING :
1439+
1440+ @overload
1441+ def query_exactly_one (self , selector : str ) -> Widget : ...
1442+
1443+ @overload
1444+ def query_exactly_one (self , selector : type [QueryType ]) -> QueryType : ...
1445+
1446+ @overload
1447+ def query_exactly_one (
1448+ self , selector : str , expect_type : type [QueryType ]
1449+ ) -> QueryType : ...
1450+
1451+ def query_exactly_one (
1452+ self ,
1453+ selector : str | type [QueryType ],
1454+ expect_type : type [QueryType ] | None = None ,
1455+ ) -> QueryType | Widget :
1456+ """Get a widget from this widget's children that matches a selector or widget type.
1457+
1458+ !!! Note
1459+ This method is similar to [query_one][textual.dom.DOMNode.query_one].
1460+ The only difference is that it will raise `TooManyMatches` if there is more than a single match.
1461+
1462+ Args:
1463+ selector: A selector or widget type.
1464+ expect_type: Require the object be of the supplied type, or None for any type.
1465+
1466+ Raises:
1467+ WrongType: If the wrong type was found.
1468+ NoMatches: If no node matches the query.
1469+ TooManyMatches: If there is more than one matching node in the query (and `exactly_one==True`).
1470+
1471+ Returns:
1472+ A widget matching the selector.
1473+ """
1474+ _rich_traceback_omit = True
1475+
1476+ if isinstance (selector , str ):
1477+ query_selector = selector
1478+ else :
1479+ query_selector = selector .__name__
1480+
1481+ selector_set = parse_selectors (query_selector )
1482+
1483+ if all (selectors .is_simple for selectors in selector_set ):
1484+ cache_key = (self ._nodes ._updates , query_selector , expect_type )
1485+ cached_result = self ._query_one_cache .get (cache_key )
1486+ if cached_result is not None :
1487+ return cached_result
1488+ else :
1489+ cache_key = None
1490+
1491+ children = walk_depth_first (self , with_root = False )
1492+ iter_children = iter (children )
1493+ for node in iter_children :
1494+ if not match (selector_set , node ):
1495+ continue
1496+ if expect_type is not None and not isinstance (node , expect_type ):
1497+ continue
1498+ for later_node in iter_children :
1499+ if match (selector_set , later_node ):
1500+ if expect_type is not None and not isinstance (node , expect_type ):
1501+ continue
1502+ raise TooManyMatches (
1503+ "Call to query_one resulted in more than one matched node"
1504+ )
1505+ if cache_key is not None :
1506+ self ._query_one_cache [cache_key ] = node
1507+ return node
1508+
1509+ raise NoMatches (f"No nodes match { selector !r} on { self !r} " )
14111510
14121511 def set_styles (self , css : str | None = None , ** update_styles : Any ) -> Self :
14131512 """Set custom styles on this object.
0 commit comments