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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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/).


## [3.7.2] - Unreleased

### Fixed

- Fixed `query_one` and `query_exactly_one` not raising documented `WrongType` exception.

## [3.7.1] - 2025-07-09

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions src/textual/css/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ def first(
if expect_type is not None:
if not isinstance(first, expect_type):
raise WrongType(
f"Query value is wrong type; expected {expect_type}, got {type(first)}"
f"Query value is the wrong type; expected type {expect_type.__name__!r}, found {first}"
)
return first
else:
Expand Down Expand Up @@ -324,7 +324,7 @@ def last(
last = self.nodes[-1]
if expect_type is not None and not isinstance(last, expect_type):
raise WrongType(
f"Query value is wrong type; expected {expect_type}, got {type(last)}"
f"Query value is the wrong type; expected type {expect_type.__name__!r}, found {last}"
)
return last

Expand Down
19 changes: 9 additions & 10 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from textual.css.errors import DeclarationError, StyleValueError
from textual.css.match import match
from textual.css.parse import parse_declarations, parse_selectors
from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches
from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches, WrongType
from textual.css.styles import RenderStyles, Styles
from textual.css.tokenize import IDENTIFIER
from textual.css.tokenizer import TokenError
Expand All @@ -65,9 +65,6 @@
from textual.widget import Widget
from textual.worker import Worker, WorkType, ResultType

# Unused & ignored imports are needed for the docs to link to these objects:
from textual.css.query import WrongType # type: ignore # noqa: F401

from typing_extensions import Literal

_re_identifier = re.compile(IDENTIFIER)
Expand Down Expand Up @@ -1488,12 +1485,14 @@ def query_one(
if not match(selector_set, node):
continue
if expect_type is not None and not isinstance(node, expect_type):
continue
raise WrongType(
f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
)
if cache_key is not None:
base_node._query_one_cache[cache_key] = node
return node

raise NoMatches(f"No nodes match {selector!r} on {self!r}")
raise NoMatches(f"No nodes match {selector!r} on {base_node!r}")

if TYPE_CHECKING:

Expand Down Expand Up @@ -1561,19 +1560,19 @@ def query_exactly_one(
if not match(selector_set, node):
continue
if expect_type is not None and not isinstance(node, expect_type):
continue
raise WrongType(
f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
)
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"
)
if cache_key is not None:
base_node._query_one_cache[cache_key] = node
return node

raise NoMatches(f"No nodes match {selector!r} on {self!r}")
raise NoMatches(f"No nodes match {selector!r} on {base_node!r}")

if TYPE_CHECKING:

Expand Down
12 changes: 5 additions & 7 deletions src/textual/getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,11 @@ def __get__(
"""Get the widget matching the selector and/or type."""
if obj is None:
return self
child = obj._nodes._get_by_id(self.child_id)
child = obj._get_dom_base()._nodes._get_by_id(self.child_id)
if child is None:
raise NoMatches(f"No child found with id={id!r}")
raise NoMatches(f"No child found with id={self.child_id!r}")
if not isinstance(child, self.expect_type):
if not isinstance(child, self.expect_type):
raise WrongType(
f"Child with id={id!r} is wrong type; expected {self.expect_type}, got"
f" {type(child)}"
)
raise WrongType(
f"Child with id={self.child_id!r} is the wrong type; expected type {self.expect_type.__name__!r}, found {child}"
)
return child
8 changes: 3 additions & 5 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,15 +998,14 @@ def get_child_by_id(
NoMatches: if no children could be found for this ID
WrongType: if the wrong type was found.
"""
child = self._nodes._get_by_id(id)
child = self._get_dom_base()._nodes._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
if not isinstance(child, expect_type):
raise WrongType(
f"Child with id={id!r} is wrong type; expected {expect_type}, got"
f" {type(child)}"
f"Child with id={id!r} is the wrong type; expected type {expect_type.__name__!r}, found {child}"
)
return child

Expand Down Expand Up @@ -1042,8 +1041,7 @@ def get_widget_by_id(
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)}"
f"Descendant with id={id!r} is the wrong type; expected type {expect_type.__name__!r}, found {widget}"
)
return widget

Expand Down
6 changes: 3 additions & 3 deletions tests/test_getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from textual.widgets import Input, Label


async def get_getters() -> None:
async def test_getters() -> None:
"""Check the getter descriptors work, and return expected errors."""

class QueryApp(App):
Expand All @@ -20,8 +20,8 @@ class QueryApp(App):

def compose(self) -> ComposeResult:
with containers.Vertical():
yield Label(id="label1", classes=".red")
yield Label(id="label2", classes=".green")
yield Label(id="label1", classes="red")
yield Label(id="label2", classes="green")

app = QueryApp()
async with app.run_test():
Expand Down
16 changes: 16 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,19 @@ def compose(self) -> ComposeResult:
# Focus non existing
app.query("#egg").focus()
assert app.focused.id == "bar"


async def test_query_error():
class QueryApp(App):
def compose(self) -> ComposeResult:
yield Input(id="foo")

app = QueryApp()
async with app.run_test():
with pytest.raises(WrongType):
# Asking for a Label, but the widget is an Input
app.query_one("#foo", Label)

# Widget is an Input so this works
foo = app.query_one("#foo", Input)
assert isinstance(foo, Input)
Loading