mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
experimental app getter
This commit is contained in:
@@ -5,12 +5,53 @@ Descriptors to define properties on your widget, screen, or App.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Generic, overload
|
from typing import TYPE_CHECKING, Generic, TypeVar, overload
|
||||||
|
|
||||||
|
from textual._context import NoActiveAppError, active_app
|
||||||
from textual.css.query import NoMatches, QueryType, WrongType
|
from textual.css.query import NoMatches, QueryType, WrongType
|
||||||
from textual.dom import DOMNode
|
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from textual.app import App
|
||||||
|
from textual.dom import DOMNode
|
||||||
|
from textual.message_pump import MessagePump
|
||||||
|
|
||||||
|
|
||||||
|
AppType = TypeVar("AppType", bound="App")
|
||||||
|
|
||||||
|
|
||||||
|
class app(Generic[AppType]):
|
||||||
|
"""Create a property to return the active app.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
class MyWidget(Widget):
|
||||||
|
app = getters.app(MyApp)
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_type: The app class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app_type: type[AppType]) -> None:
|
||||||
|
self._app_type = app_type
|
||||||
|
|
||||||
|
def __get__(self, obj: MessagePump, obj_type: type[MessagePump]) -> AppType:
|
||||||
|
try:
|
||||||
|
app = active_app.get()
|
||||||
|
except LookupError:
|
||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
node: MessagePump | None = obj
|
||||||
|
while not isinstance(node, App):
|
||||||
|
if node is None:
|
||||||
|
raise NoActiveAppError()
|
||||||
|
node = node._parent
|
||||||
|
app = node
|
||||||
|
|
||||||
|
assert isinstance(app, self._app_type)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
class query_one(Generic[QueryType]):
|
class query_one(Generic[QueryType]):
|
||||||
"""Create a query one property.
|
"""Create a query one property.
|
||||||
@@ -45,7 +86,7 @@ class query_one(Generic[QueryType]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
selector: str
|
selector: str
|
||||||
expect_type: type[Widget]
|
expect_type: type["Widget"]
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, selector: str) -> None:
|
def __init__(self, selector: str) -> None:
|
||||||
@@ -72,6 +113,8 @@ class query_one(Generic[QueryType]):
|
|||||||
expect_type: type[QueryType] | None = None,
|
expect_type: type[QueryType] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if expect_type is None:
|
if expect_type is None:
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
self.expect_type = Widget
|
self.expect_type = Widget
|
||||||
else:
|
else:
|
||||||
self.expect_type = expect_type
|
self.expect_type = expect_type
|
||||||
|
|||||||
@@ -227,29 +227,35 @@ class MessagePump(metaclass=_MessagePumpMeta):
|
|||||||
"""Is this a root node (i.e. the App)?"""
|
"""Is this a root node (i.e. the App)?"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
if TYPE_CHECKING:
|
||||||
def app(self) -> "App[object]":
|
from textual import getters
|
||||||
"""
|
|
||||||
Get the current app.
|
|
||||||
|
|
||||||
Returns:
|
app = getters.app(App)
|
||||||
The current app.
|
else:
|
||||||
|
|
||||||
Raises:
|
@property
|
||||||
NoActiveAppError: if no active app could be found for the current asyncio context
|
def app(self) -> "App[object]":
|
||||||
"""
|
"""
|
||||||
try:
|
Get the current app.
|
||||||
return active_app.get()
|
|
||||||
except LookupError:
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
node: MessagePump | None = self
|
Returns:
|
||||||
while not isinstance(node, App):
|
The current app.
|
||||||
if node is None:
|
|
||||||
raise NoActiveAppError()
|
|
||||||
node = node._parent
|
|
||||||
|
|
||||||
return node
|
Raises:
|
||||||
|
NoActiveAppError: if no active app could be found for the current asyncio context
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return active_app.get()
|
||||||
|
except LookupError:
|
||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
node: MessagePump | None = self
|
||||||
|
while not isinstance(node, App):
|
||||||
|
if node is None:
|
||||||
|
raise NoActiveAppError()
|
||||||
|
node = node._parent
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_attached(self) -> bool:
|
def is_attached(self) -> bool:
|
||||||
|
|||||||
@@ -662,6 +662,11 @@ TextArea {
|
|||||||
self._line_cache.clear()
|
self._line_cache.clear()
|
||||||
super().notify_style_update()
|
super().notify_style_update()
|
||||||
|
|
||||||
|
def update_suggestion(self) -> None:
|
||||||
|
"""A hook called after edits, to allow subclasses to update the
|
||||||
|
[`suggestion`][textual.widgets.TextArea.suggestion] attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
def check_consume_key(self, key: str, character: str | None = None) -> bool:
|
def check_consume_key(self, key: str, character: str | None = None) -> bool:
|
||||||
"""Check if the widget may consume the given key.
|
"""Check if the widget may consume the given key.
|
||||||
|
|
||||||
@@ -1534,7 +1539,10 @@ TextArea {
|
|||||||
Data relating to the edit that may be useful. The data returned
|
Data relating to the edit that may be useful. The data returned
|
||||||
may be different depending on the edit performed.
|
may be different depending on the edit performed.
|
||||||
"""
|
"""
|
||||||
self.suggestion = ""
|
if self.suggestion.startswith(edit.text):
|
||||||
|
self.suggestion = self.suggestion[len(edit.text) :]
|
||||||
|
else:
|
||||||
|
self.suggestion = ""
|
||||||
old_gutter_width = self.gutter_width
|
old_gutter_width = self.gutter_width
|
||||||
result = edit.do(self)
|
result = edit.do(self)
|
||||||
self.history.record(edit)
|
self.history.record(edit)
|
||||||
@@ -1553,6 +1561,7 @@ TextArea {
|
|||||||
edit.after(self)
|
edit.after(self)
|
||||||
self._build_highlight_map()
|
self._build_highlight_map()
|
||||||
self.post_message(self.Changed(self))
|
self.post_message(self.Changed(self))
|
||||||
|
self.update_suggestion()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
|
|||||||
@@ -43,3 +43,19 @@ async def test_getters() -> None:
|
|||||||
|
|
||||||
with pytest.raises(NoMatches):
|
with pytest.raises(NoMatches):
|
||||||
app.label2_missing
|
app.label2_missing
|
||||||
|
|
||||||
|
|
||||||
|
async def test_app_getter() -> None:
|
||||||
|
|
||||||
|
class MyApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
my_widget = MyWidget()
|
||||||
|
my_widget.app
|
||||||
|
yield my_widget
|
||||||
|
|
||||||
|
class MyWidget(Widget):
|
||||||
|
app = getters.app(MyApp)
|
||||||
|
|
||||||
|
app = MyApp()
|
||||||
|
async with app.run_test():
|
||||||
|
assert isinstance(app.query_one(MyWidget).app, MyApp)
|
||||||
|
|||||||
Reference in New Issue
Block a user