Skip to content

Commit 8178119

Browse files
authored
Merge pull request #5945 from Textualize/fix-query-wrong-type
Fix query wrong type
2 parents 64c2c2e + 663c2fa commit 8178119

File tree

7 files changed

+45
-27
lines changed

7 files changed

+45
-27
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
9+
## [3.7.2] - Unreleased
10+
11+
### Fixed
12+
13+
- Fixed `query_one` and `query_exactly_one` not raising documented `WrongType` exception.
14+
815
## [3.7.1] - 2025-07-09
916

1017
### Fixed

src/textual/css/query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def first(
242242
if expect_type is not None:
243243
if not isinstance(first, expect_type):
244244
raise WrongType(
245-
f"Query value is wrong type; expected {expect_type}, got {type(first)}"
245+
f"Query value is the wrong type; expected type {expect_type.__name__!r}, found {first}"
246246
)
247247
return first
248248
else:
@@ -324,7 +324,7 @@ def last(
324324
last = self.nodes[-1]
325325
if expect_type is not None and not isinstance(last, expect_type):
326326
raise WrongType(
327-
f"Query value is wrong type; expected {expect_type}, got {type(last)}"
327+
f"Query value is the wrong type; expected type {expect_type.__name__!r}, found {last}"
328328
)
329329
return last
330330

src/textual/dom.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from textual.css.errors import DeclarationError, StyleValueError
4242
from textual.css.match import match
4343
from textual.css.parse import parse_declarations, parse_selectors
44-
from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches
44+
from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches, WrongType
4545
from textual.css.styles import RenderStyles, Styles
4646
from textual.css.tokenize import IDENTIFIER
4747
from textual.css.tokenizer import TokenError
@@ -65,9 +65,6 @@
6565
from textual.widget import Widget
6666
from textual.worker import Worker, WorkType, ResultType
6767

68-
# Unused & ignored imports are needed for the docs to link to these objects:
69-
from textual.css.query import WrongType # type: ignore # noqa: F401
70-
7168
from typing_extensions import Literal
7269

7370
_re_identifier = re.compile(IDENTIFIER)
@@ -1488,12 +1485,14 @@ def query_one(
14881485
if not match(selector_set, node):
14891486
continue
14901487
if expect_type is not None and not isinstance(node, expect_type):
1491-
continue
1488+
raise WrongType(
1489+
f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
1490+
)
14921491
if cache_key is not None:
14931492
base_node._query_one_cache[cache_key] = node
14941493
return node
14951494

1496-
raise NoMatches(f"No nodes match {selector!r} on {self!r}")
1495+
raise NoMatches(f"No nodes match {selector!r} on {base_node!r}")
14971496

14981497
if TYPE_CHECKING:
14991498

@@ -1561,19 +1560,19 @@ def query_exactly_one(
15611560
if not match(selector_set, node):
15621561
continue
15631562
if expect_type is not None and not isinstance(node, expect_type):
1564-
continue
1563+
raise WrongType(
1564+
f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
1565+
)
15651566
for later_node in iter_children:
15661567
if match(selector_set, later_node):
1567-
if expect_type is not None and not isinstance(node, expect_type):
1568-
continue
15691568
raise TooManyMatches(
15701569
"Call to query_one resulted in more than one matched node"
15711570
)
15721571
if cache_key is not None:
15731572
base_node._query_one_cache[cache_key] = node
15741573
return node
15751574

1576-
raise NoMatches(f"No nodes match {selector!r} on {self!r}")
1575+
raise NoMatches(f"No nodes match {selector!r} on {base_node!r}")
15771576

15781577
if TYPE_CHECKING:
15791578

src/textual/getters.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,11 @@ def __get__(
176176
"""Get the widget matching the selector and/or type."""
177177
if obj is None:
178178
return self
179-
child = obj._nodes._get_by_id(self.child_id)
179+
child = obj._get_dom_base()._nodes._get_by_id(self.child_id)
180180
if child is None:
181-
raise NoMatches(f"No child found with id={id!r}")
181+
raise NoMatches(f"No child found with id={self.child_id!r}")
182182
if not isinstance(child, self.expect_type):
183-
if not isinstance(child, self.expect_type):
184-
raise WrongType(
185-
f"Child with id={id!r} is wrong type; expected {self.expect_type}, got"
186-
f" {type(child)}"
187-
)
183+
raise WrongType(
184+
f"Child with id={self.child_id!r} is the wrong type; expected type {self.expect_type.__name__!r}, found {child}"
185+
)
188186
return child

src/textual/widget.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -998,15 +998,14 @@ def get_child_by_id(
998998
NoMatches: if no children could be found for this ID
999999
WrongType: if the wrong type was found.
10001000
"""
1001-
child = self._nodes._get_by_id(id)
1001+
child = self._get_dom_base()._nodes._get_by_id(id)
10021002
if child is None:
10031003
raise NoMatches(f"No child found with id={id!r}")
10041004
if expect_type is None:
10051005
return child
10061006
if not isinstance(child, expect_type):
10071007
raise WrongType(
1008-
f"Child with id={id!r} is wrong type; expected {expect_type}, got"
1009-
f" {type(child)}"
1008+
f"Child with id={id!r} is the wrong type; expected type {expect_type.__name__!r}, found {child}"
10101009
)
10111010
return child
10121011

@@ -1042,8 +1041,7 @@ def get_widget_by_id(
10421041
widget = self.query_one(f"#{id}")
10431042
if expect_type is not None and not isinstance(widget, expect_type):
10441043
raise WrongType(
1045-
f"Descendant with id={id!r} is wrong type; expected {expect_type},"
1046-
f" got {type(widget)}"
1044+
f"Descendant with id={id!r} is the wrong type; expected type {expect_type.__name__!r}, found {widget}"
10471045
)
10481046
return widget
10491047

tests/test_getters.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from textual.widgets import Input, Label
88

99

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

1313
class QueryApp(App):
@@ -20,8 +20,8 @@ class QueryApp(App):
2020

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

2626
app = QueryApp()
2727
async with app.run_test():

tests/test_query.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,19 @@ def compose(self) -> ComposeResult:
364364
# Focus non existing
365365
app.query("#egg").focus()
366366
assert app.focused.id == "bar"
367+
368+
369+
async def test_query_error():
370+
class QueryApp(App):
371+
def compose(self) -> ComposeResult:
372+
yield Input(id="foo")
373+
374+
app = QueryApp()
375+
async with app.run_test():
376+
with pytest.raises(WrongType):
377+
# Asking for a Label, but the widget is an Input
378+
app.query_one("#foo", Label)
379+
380+
# Widget is an Input so this works
381+
foo = app.query_one("#foo", Input)
382+
assert isinstance(foo, Input)

0 commit comments

Comments
 (0)