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 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.dom import DOMNode
|
||||
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]):
|
||||
"""Create a query one property.
|
||||
@@ -45,7 +86,7 @@ class query_one(Generic[QueryType]):
|
||||
"""
|
||||
|
||||
selector: str
|
||||
expect_type: type[Widget]
|
||||
expect_type: type["Widget"]
|
||||
|
||||
@overload
|
||||
def __init__(self, selector: str) -> None:
|
||||
@@ -72,6 +113,8 @@ class query_one(Generic[QueryType]):
|
||||
expect_type: type[QueryType] | None = None,
|
||||
) -> None:
|
||||
if expect_type is None:
|
||||
from textual.widget import Widget
|
||||
|
||||
self.expect_type = Widget
|
||||
else:
|
||||
self.expect_type = expect_type
|
||||
|
||||
@@ -227,29 +227,35 @@ class MessagePump(metaclass=_MessagePumpMeta):
|
||||
"""Is this a root node (i.e. the App)?"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def app(self) -> "App[object]":
|
||||
"""
|
||||
Get the current app.
|
||||
if TYPE_CHECKING:
|
||||
from textual import getters
|
||||
|
||||
Returns:
|
||||
The current app.
|
||||
app = getters.app(App)
|
||||
else:
|
||||
|
||||
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
|
||||
@property
|
||||
def app(self) -> "App[object]":
|
||||
"""
|
||||
Get the current app.
|
||||
|
||||
node: MessagePump | None = self
|
||||
while not isinstance(node, App):
|
||||
if node is None:
|
||||
raise NoActiveAppError()
|
||||
node = node._parent
|
||||
Returns:
|
||||
The current app.
|
||||
|
||||
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
|
||||
def is_attached(self) -> bool:
|
||||
|
||||
@@ -662,6 +662,11 @@ TextArea {
|
||||
self._line_cache.clear()
|
||||
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:
|
||||
"""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
|
||||
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
|
||||
result = edit.do(self)
|
||||
self.history.record(edit)
|
||||
@@ -1553,6 +1561,7 @@ TextArea {
|
||||
edit.after(self)
|
||||
self._build_highlight_map()
|
||||
self.post_message(self.Changed(self))
|
||||
self.update_suggestion()
|
||||
return result
|
||||
|
||||
def undo(self) -> None:
|
||||
|
||||
@@ -43,3 +43,19 @@ async def test_getters() -> None:
|
||||
|
||||
with pytest.raises(NoMatches):
|
||||
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