Merge pull request #1102 from davep/there-can-only-be-one

Change query_one so that it raises an error on multiple hits
This commit is contained in:
Dave Pearson
2022-11-03 15:36:05 +00:00
committed by GitHub
4 changed files with 57 additions and 5 deletions

View File

@@ -12,11 +12,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Dropped support for mounting "named" and "anonymous" widgets via - Dropped support for mounting "named" and "anonymous" widgets via
`App.mount` and `Widget.mount`. Both methods now simply take one or more `App.mount` and `Widget.mount`. Both methods now simply take one or more
widgets as positional arguments. widgets as positional arguments.
- `DOMNode.query_one` now raises a `TooManyMatches` exception if there is
more than one matching node.
https://github.com/Textualize/textual/issues/1096
### Added ### Added
- Added `init` param to reactive.watch - Added `init` param to reactive.watch
- `CSS_PATH` can now be a list of CSS files https://github.com/Textualize/textual/pull/1079 - `CSS_PATH` can now be a list of CSS files https://github.com/Textualize/textual/pull/1079
- Added `DOMQuery.only_one` https://github.com/Textualize/textual/issues/1096
## [0.3.0] - 2022-10-31 ## [0.3.0] - 2022-10-31

View File

@@ -42,6 +42,10 @@ class NoMatches(QueryError):
"""No nodes matched the query.""" """No nodes matched the query."""
class TooManyMatches(QueryError):
"""Too many nodes matched the query."""
class WrongType(QueryError): class WrongType(QueryError):
"""Query result was not of the correct type.""" """Query result was not of the correct type."""
@@ -208,6 +212,49 @@ class DOMQuery(Generic[QueryType]):
else: else:
raise NoMatches(f"No nodes match {self!r}") raise NoMatches(f"No nodes match {self!r}")
@overload
def only_one(self) -> Widget:
...
@overload
def only_one(self, expect_type: type[ExpectType]) -> ExpectType:
...
def only_one(
self, expect_type: type[ExpectType] | None = None
) -> Widget | ExpectType:
"""Get the *only* matching node.
Args:
expect_type (type[ExpectType] | None, optional): Require matched node is of this type,
or None for any type. Defaults to None.
Raises:
WrongType: If the wrong type was found.
TooManyMatches: If there is more than one matching node in the query.
Returns:
Widget | ExpectType: The matching Widget.
"""
# Call on first to get the first item. Here we'll use all of the
# testing and checking it provides.
the_one = self.first(expect_type) if expect_type is not None else self.first()
try:
# Now see if we can access a subsequent item in the nodes. There
# should *not* be anything there, so we *should* get an
# IndexError. We *could* have just checked the length of the
# query, but the idea here is to do the check as cheaply as
# possible.
_ = self.nodes[1]
raise TooManyMatches(
"Call to only_one resulted in more than one matched node"
)
except IndexError:
# The IndexError was got, that's a good thing in this case. So
# we return what we found.
pass
return the_one
@overload @overload
def last(self) -> Widget: def last(self) -> Widget:
... ...

View File

@@ -783,10 +783,7 @@ class DOMNode(MessagePump):
query_selector = selector.__name__ query_selector = selector.__name__
query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector) query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)
if expect_type is None: return query.only_one() if expect_type is None else query.only_one(expect_type)
return query.first()
else:
return query.first(expect_type)
def set_styles(self, css: str | None = None, **update_styles) -> None: def set_styles(self, css: str | None = None, **update_styles) -> None:
"""Set custom styles on this object.""" """Set custom styles on this object."""

View File

@@ -1,7 +1,7 @@
import pytest import pytest
from textual.widget import Widget from textual.widget import Widget
from textual.css.query import InvalidQueryFormat, WrongType, NoMatches from textual.css.query import InvalidQueryFormat, WrongType, NoMatches, TooManyMatches
def test_query(): def test_query():
@@ -82,6 +82,10 @@ def test_query():
helpbar, helpbar,
] ]
assert list(app.query("Widget.float").results(View)) == [] assert list(app.query("Widget.float").results(View)) == []
assert app.query_one("#widget1") == widget1
assert app.query_one("#widget1", Widget) == widget1
with pytest.raises(TooManyMatches):
_ = app.query_one(Widget)
assert app.query("Widget.float")[0] == sidebar assert app.query("Widget.float")[0] == sidebar
assert app.query("Widget.float")[0:2] == [sidebar, tooltip] assert app.query("Widget.float")[0:2] == [sidebar, tooltip]