mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user