From 2469cf69261ec6d275bd279ee65c569ef7eb7876 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 08:32:12 +0100 Subject: [PATCH 01/12] fix getters --- src/textual/dom.py | 13 +++++++------ src/textual/getters.py | 2 +- tests/test_getters.py | 31 +++++++++++++++++++------------ 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index c91f6cfc0b..d863fa0a33 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,7 +1485,9 @@ 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 not of expected type {expect_type.__name__!r}; found {node}" + ) if cache_key is not None: base_node._query_one_cache[cache_key] = node return node @@ -1565,7 +1564,9 @@ def query_exactly_one( 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 WrongType( + f"Node matching {query_selector!r} is not of expected type {expect_type.__name__!r}; found {node}" + ) raise TooManyMatches( "Call to query_one resulted in more than one matched node" ) diff --git a/src/textual/getters.py b/src/textual/getters.py index f5b23590b0..1d31c34f2c 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -178,7 +178,7 @@ def __get__( return self child = obj._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( diff --git a/tests/test_getters.py b/tests/test_getters.py index 4ccfbf85e1..39f60746a1 100644 --- a/tests/test_getters.py +++ b/tests/test_getters.py @@ -3,14 +3,15 @@ from textual import containers, getters from textual.app import App, ComposeResult from textual.css.query import NoMatches, WrongType +from textual.screen import Screen from textual.widget import Widget 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): + class GetterScreen(Screen): label1 = getters.query_one("#label1", Label) label2 = getters.child_by_id("label2", Label) label1_broken = getters.query_one("#label1", Input) @@ -20,26 +21,32 @@ 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") + + class QueryApp(App): + + def get_default_screen(self) -> Screen: + return GetterScreen() app = QueryApp() async with app.run_test(): + screen: GetterScreen = app.screen - assert isinstance(app.label1, Label) - assert app.label1.id == "label1" + assert isinstance(screen.label1, Label) + assert screen.label1.id == "label1" - assert isinstance(app.label2, Label) - assert app.label2.id == "label2" + assert isinstance(screen.label2, Label) + assert screen.label2.id == "label2" with pytest.raises(WrongType): - app.label1_broken + screen.label1_broken with pytest.raises(WrongType): - app.label2_broken + screen.label2_broken with pytest.raises(NoMatches): - app.label1_missing + screen.label1_missing with pytest.raises(NoMatches): - app.label2_missing + screen.label2_missing From e4197852806f20d18eb8eed94d5e3a2fc94cf0e0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 08:39:16 +0100 Subject: [PATCH 02/12] fix wrong type --- src/textual/getters.py | 2 +- src/textual/widget.py | 2 +- tests/test_getters.py | 26 ++++++++++---------------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/textual/getters.py b/src/textual/getters.py index 1d31c34f2c..fee23b3bb2 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -176,7 +176,7 @@ 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={self.child_id!r}") if not isinstance(child, self.expect_type): diff --git a/src/textual/widget.py b/src/textual/widget.py index cf0ec8a964..7f51cbab33 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -998,7 +998,7 @@ 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: diff --git a/tests/test_getters.py b/tests/test_getters.py index 39f60746a1..d428d1f684 100644 --- a/tests/test_getters.py +++ b/tests/test_getters.py @@ -3,7 +3,6 @@ from textual import containers, getters from textual.app import App, ComposeResult from textual.css.query import NoMatches, WrongType -from textual.screen import Screen from textual.widget import Widget from textual.widgets import Input, Label @@ -11,7 +10,8 @@ async def test_getters() -> None: """Check the getter descriptors work, and return expected errors.""" - class GetterScreen(Screen): + class QueryApp(App): + label1 = getters.query_one("#label1", Label) label2 = getters.child_by_id("label2", Label) label1_broken = getters.query_one("#label1", Input) @@ -24,29 +24,23 @@ def compose(self) -> ComposeResult: yield Label(id="label1", classes="red") yield Label(id="label2", classes="green") - class QueryApp(App): - - def get_default_screen(self) -> Screen: - return GetterScreen() - app = QueryApp() async with app.run_test(): - screen: GetterScreen = app.screen - assert isinstance(screen.label1, Label) - assert screen.label1.id == "label1" + assert isinstance(app.label1, Label) + assert app.label1.id == "label1" - assert isinstance(screen.label2, Label) - assert screen.label2.id == "label2" + assert isinstance(app.label2, Label) + assert app.label2.id == "label2" with pytest.raises(WrongType): - screen.label1_broken + app.label1_broken with pytest.raises(WrongType): - screen.label2_broken + app.label2_broken with pytest.raises(NoMatches): - screen.label1_missing + app.label1_missing with pytest.raises(NoMatches): - screen.label2_missing + app.label2_missing From f3842c6789624476937d8874be440e9c6f23f736 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 08:48:23 +0100 Subject: [PATCH 03/12] test --- CHANGELOG.md | 7 +++++++ src/textual/getters.py | 9 ++++----- tests/test_query.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d93ccc06..4cfe4d72ca 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] - 2025-07-10 + +### 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/getters.py b/src/textual/getters.py index fee23b3bb2..1e090b27f3 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -180,9 +180,8 @@ def __get__( if child is None: 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={id!r} is wrong type; expected {self.expect_type}, got" + f" {type(child)}" + ) return child 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) From bf309889b4b2c22440ab16d6b5299d61ead5f38f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 08:50:20 +0100 Subject: [PATCH 04/12] error message --- src/textual/dom.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index d863fa0a33..3362d0f62e 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1486,7 +1486,7 @@ def query_one( continue if expect_type is not None and not isinstance(node, expect_type): raise WrongType( - f"Node matching {query_selector!r} is not of expected type {expect_type.__name__!r}; found {node}" + 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 @@ -1560,13 +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; expect 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): - raise WrongType( - f"Node matching {query_selector!r} is not of expected type {expect_type.__name__!r}; found {node}" - ) raise TooManyMatches( "Call to query_one resulted in more than one matched node" ) From b664391739489cfbbfa4718eeb1db804dd93f76a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 08:51:36 +0100 Subject: [PATCH 05/12] wording --- src/textual/dom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 3362d0f62e..01f740b1b4 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1561,7 +1561,7 @@ def query_exactly_one( continue if expect_type is not None and not isinstance(node, expect_type): raise WrongType( - f"Node matching {query_selector!r} is the wrong type; expect type {expect_type.__name__!r}; found {node}" + 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): From 8d599f98eae34872d402af899f109318e6f2a229 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 08:54:38 +0100 Subject: [PATCH 06/12] error messages --- src/textual/css/query.py | 4 ++-- src/textual/getters.py | 3 +-- src/textual/widget.py | 6 ++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 8250afb2d3..8cd16f2120 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 {expect_type}, 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 {expect_type}, found {last}" ) return last diff --git a/src/textual/getters.py b/src/textual/getters.py index 1e090b27f3..a0ba8b7982 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -181,7 +181,6 @@ def __get__( raise NoMatches(f"No child found with id={self.child_id!r}") 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)}" + f"Child with id={id!r} is the wrong type; expected type {self.expect_type}, found {child}" ) return child diff --git a/src/textual/widget.py b/src/textual/widget.py index 7f51cbab33..e14f58d9e3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1005,8 +1005,7 @@ def get_child_by_id( 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}, 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 {expect_type}, found {widget}" ) return widget From 5b2b9fde3116fddbebf5bbfa81e72718c215bd88 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 09:02:27 +0100 Subject: [PATCH 07/12] error messages --- src/textual/dom.py | 4 ++-- src/textual/getters.py | 2 +- src/textual/widget.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 01f740b1b4..762980f2e1 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1492,7 +1492,7 @@ def query_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: @@ -1572,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 a0ba8b7982..5ad09dda2d 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -181,6 +181,6 @@ def __get__( raise NoMatches(f"No child found with id={self.child_id!r}") if not isinstance(child, self.expect_type): raise WrongType( - f"Child with id={id!r} is the wrong type; expected type {self.expect_type}, found {child}" + f"Child with id={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 e14f58d9e3..0997ce217d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1005,7 +1005,7 @@ def get_child_by_id( return child if not isinstance(child, expect_type): raise WrongType( - f"Child with id={id!r} is the wrong type; expected type {expect_type}, found {child}" + f"Child with id={id!r} is the wrong type; expected type {expect_type.__name__!r}, found {child}" ) return child @@ -1041,7 +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 the wrong type; expected {expect_type}, found {widget}" + f"Descendant with id={id!r} is the wrong type; expected type {expect_type.__name__!r}, found {widget}" ) return widget From e8789ac545bd9ade018b8865079f0ba873850d2e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 09:10:08 +0100 Subject: [PATCH 08/12] comma --- src/textual/css/_style_properties.py | 2 +- src/textual/dom.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 7860eb32e0..9408be5c58 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -1219,7 +1219,7 @@ def __set__( character = HATCHES[character] except KeyError: raise ValueError( - f"Expected a character or hatch value here; found {character!r}" + f"Expected a character or hatch value here, found {character!r}" ) from None if cell_len(character) != 1: raise ValueError("Hatch character must have a cell length of 1") diff --git a/src/textual/dom.py b/src/textual/dom.py index 762980f2e1..6be6c331ca 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1486,7 +1486,7 @@ def query_one( continue if expect_type is not None and not isinstance(node, expect_type): raise WrongType( - f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}; found {node}" + 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 @@ -1561,7 +1561,7 @@ def query_exactly_one( continue if expect_type is not None and not isinstance(node, expect_type): raise WrongType( - f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}; found {node}" + 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): From 984385bf1a8cbd0a4e939cbb07c7c83ea5221b56 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 09:11:19 +0100 Subject: [PATCH 09/12] ws --- tests/test_getters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_getters.py b/tests/test_getters.py index d428d1f684..f92173e94c 100644 --- a/tests/test_getters.py +++ b/tests/test_getters.py @@ -11,7 +11,6 @@ async def test_getters() -> None: """Check the getter descriptors work, and return expected errors.""" class QueryApp(App): - label1 = getters.query_one("#label1", Label) label2 = getters.child_by_id("label2", Label) label1_broken = getters.query_one("#label1", Input) From 7bfdc5d1480917e27d5a9accba623b5281e9f9f6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 09:11:53 +0100 Subject: [PATCH 10/12] punctuation --- src/textual/css/_style_properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 9408be5c58..7860eb32e0 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -1219,7 +1219,7 @@ def __set__( character = HATCHES[character] except KeyError: raise ValueError( - f"Expected a character or hatch value here, found {character!r}" + f"Expected a character or hatch value here; found {character!r}" ) from None if cell_len(character) != 1: raise ValueError("Hatch character must have a cell length of 1") From 363fe22b38bd1b147756833cbb652f253532be01 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 10:40:07 +0100 Subject: [PATCH 11/12] words --- CHANGELOG.md | 2 +- src/textual/css/query.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfe4d72ca..a225adf3b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [3.7.2] - 2025-07-10 +## [3.7.2] - Unreleased ### Fixed diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 8cd16f2120..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 the wrong type; expected {expect_type}, found {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 the wrong type; expected {expect_type}, found {last}" + f"Query value is the wrong type; expected type {expect_type.__name__!r}, found {last}" ) return last From 663c2fa6cf4cd1553fc25d1f80df0fa42a272990 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 10 Jul 2025 10:41:21 +0100 Subject: [PATCH 12/12] Update src/textual/getters.py Co-authored-by: TomJGooding <101601846+TomJGooding@users.noreply.github.com> --- src/textual/getters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/getters.py b/src/textual/getters.py index 5ad09dda2d..02d3dce907 100644 --- a/src/textual/getters.py +++ b/src/textual/getters.py @@ -181,6 +181,6 @@ def __get__( raise NoMatches(f"No child found with id={self.child_id!r}") if not isinstance(child, self.expect_type): raise WrongType( - f"Child with id={id!r} is the wrong type; expected type {self.expect_type.__name__!r}, found {child}" + f"Child with id={self.child_id!r} is the wrong type; expected type {self.expect_type.__name__!r}, found {child}" ) return child