diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f57145b9..6210a167ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Added + +- Added `DOM.query_one_optional` + ## [7.2.0] - 2026-01-11 ### Changed diff --git a/src/textual/dom.py b/src/textual/dom.py index 5e35a4f847..fa36ce767c 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1526,6 +1526,43 @@ def query_one( raise NoMatches(f"No nodes match {query_selector!r} on {base_node!r}") + if TYPE_CHECKING: + + @overload + def query_one_optional(self, selector: str) -> Widget | None: ... + + @overload + def query_one_optional(self, selector: type[QueryType]) -> QueryType | None: ... + + @overload + def query_one_optional( + self, selector: str, expect_type: type[QueryType] + ) -> QueryType | None: ... + + def query_one_optional( + self, + selector: str | type[QueryType], + expect_type: type[QueryType] | None = None, + ) -> QueryType | Widget | None: + """Get a widget from this widget's children that matches a selector or widget type, + or `None` if there is no 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. + + Returns: + A widget matching the selector, or `None`. + """ + try: + widget = self.query_one(selector, expect_type) + except NoMatches: + return None + return widget + if TYPE_CHECKING: @overload diff --git a/tests/test_query.py b/tests/test_query.py index 8d5ede184f..b9dcbc6f8e 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -380,3 +380,20 @@ def compose(self) -> ComposeResult: # Widget is an Input so this works foo = app.query_one("#foo", Input) assert isinstance(foo, Input) + + +async def test_query_one_optional(): + class QueryApp(App): + AUTO_FOCUS = None + + def compose(self) -> ComposeResult: + yield Input(id="foo") + yield Input(classes="bar") + + app = QueryApp() + async with app.run_test(): + assert app.query_one_optional("TextArea") is None + assert app.query_one_optional("Input#bar") is None + + assert isinstance(app.query_one_optional("Input"), Input) + assert isinstance(app.query_one_optional(".bar"), Input)