Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions src/textual/getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,53 @@

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.
Expand Down Expand Up @@ -45,7 +86,7 @@ def on_mount(self) -> None:
"""

selector: str
expect_type: type[Widget]
expect_type: type["Widget"]

@overload
def __init__(self, selector: str) -> None:
Expand All @@ -72,6 +113,8 @@ def __init__(
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
Expand Down
44 changes: 25 additions & 19 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,29 +227,35 @@ def is_dom_root(self):
"""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.

Returns:
The current app.

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
node: MessagePump | None = self
while not isinstance(node, App):
if node is None:
raise NoActiveAppError()
node = node._parent

return node
return node

@property
def is_attached(self) -> bool:
Expand Down
11 changes: 10 additions & 1 deletion src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,11 @@ def notify_style_update(self) -> None:
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.

Expand Down Expand Up @@ -1534,7 +1539,10 @@ def edit(self, edit: Edit) -> EditResult:
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)
Expand All @@ -1553,6 +1561,7 @@ def edit(self, edit: Edit) -> EditResult:
edit.after(self)
self._build_highlight_map()
self.post_message(self.Changed(self))
self.update_suggestion()
return result

def undo(self) -> None:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ def compose(self) -> ComposeResult:

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