experimental app getter

This commit is contained in:
Will McGugan
2025-08-31 11:38:12 +01:00
parent dcc495ab42
commit 995c2b7e6d
4 changed files with 97 additions and 23 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)