Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +395 to +399
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test does not verify that WrongType exceptions still propagate as documented in the docstring. Consider adding a test case similar to test_query_error that verifies query_one_optional raises WrongType when a widget is found but has the wrong type (e.g., app.query_one_optional('#foo', Label) should raise WrongType when #foo is an Input).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Loading