diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d93ccc06..a225adf3b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 8250afb2d3..660190cbf5 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -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: @@ -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 diff --git a/src/textual/dom.py b/src/textual/dom.py index c91f6cfc0b..6be6c331ca 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -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 @@ -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) @@ -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: @@ -1561,11 +1560,11 @@ 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" ) @@ -1573,7 +1572,7 @@ def query_exactly_one( 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: diff --git a/src/textual/getters.py b/src/textual/getters.py index f5b23590b0..02d3dce907 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -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 diff --git a/src/textual/widget.py b/src/textual/widget.py index cf0ec8a964..0997ce217d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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 @@ -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 diff --git a/tests/test_getters.py b/tests/test_getters.py index 4ccfbf85e1..f92173e94c 100644 --- a/tests/test_getters.py +++ b/tests/test_getters.py @@ -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): @@ -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(): diff --git a/tests/test_query.py b/tests/test_query.py index 0c0759b653..5a1077a0e2 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -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)