Skip to content

Commit d34fc26

Browse files
authored
Merge branch 'main' into update-suggestion
2 parents db81195 + cfcdaef commit d34fc26

File tree

4 files changed

+87
-24
lines changed

4 files changed

+87
-24
lines changed

src/textual/getters.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,53 @@
55

66
from __future__ import annotations
77

8-
from typing import Generic, overload
8+
from typing import TYPE_CHECKING, Generic, TypeVar, overload
99

10+
from textual._context import NoActiveAppError, active_app
1011
from textual.css.query import NoMatches, QueryType, WrongType
11-
from textual.dom import DOMNode
1212
from textual.widget import Widget
1313

14+
if TYPE_CHECKING:
15+
from textual.app import App
16+
from textual.dom import DOMNode
17+
from textual.message_pump import MessagePump
18+
19+
20+
AppType = TypeVar("AppType", bound="App")
21+
22+
23+
class app(Generic[AppType]):
24+
"""Create a property to return the active app.
25+
26+
Example:
27+
```python
28+
class MyWidget(Widget):
29+
app = getters.app(MyApp)
30+
```
31+
32+
Args:
33+
app_type: The app class.
34+
"""
35+
36+
def __init__(self, app_type: type[AppType]) -> None:
37+
self._app_type = app_type
38+
39+
def __get__(self, obj: MessagePump, obj_type: type[MessagePump]) -> AppType:
40+
try:
41+
app = active_app.get()
42+
except LookupError:
43+
from textual.app import App
44+
45+
node: MessagePump | None = obj
46+
while not isinstance(node, App):
47+
if node is None:
48+
raise NoActiveAppError()
49+
node = node._parent
50+
app = node
51+
52+
assert isinstance(app, self._app_type)
53+
return app
54+
1455

1556
class query_one(Generic[QueryType]):
1657
"""Create a query one property.
@@ -45,7 +86,7 @@ def on_mount(self) -> None:
4586
"""
4687

4788
selector: str
48-
expect_type: type[Widget]
89+
expect_type: type["Widget"]
4990

5091
@overload
5192
def __init__(self, selector: str) -> None:
@@ -72,6 +113,8 @@ def __init__(
72113
expect_type: type[QueryType] | None = None,
73114
) -> None:
74115
if expect_type is None:
116+
from textual.widget import Widget
117+
75118
self.expect_type = Widget
76119
else:
77120
self.expect_type = expect_type

src/textual/message_pump.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -227,29 +227,35 @@ def is_dom_root(self):
227227
"""Is this a root node (i.e. the App)?"""
228228
return False
229229

230-
@property
231-
def app(self) -> "App[object]":
232-
"""
233-
Get the current app.
230+
if TYPE_CHECKING:
231+
from textual import getters
234232

235-
Returns:
236-
The current app.
233+
app = getters.app(App)
234+
else:
237235

238-
Raises:
239-
NoActiveAppError: if no active app could be found for the current asyncio context
240-
"""
241-
try:
242-
return active_app.get()
243-
except LookupError:
244-
from textual.app import App
236+
@property
237+
def app(self) -> "App[object]":
238+
"""
239+
Get the current app.
240+
241+
Returns:
242+
The current app.
243+
244+
Raises:
245+
NoActiveAppError: if no active app could be found for the current asyncio context
246+
"""
247+
try:
248+
return active_app.get()
249+
except LookupError:
250+
from textual.app import App
245251

246-
node: MessagePump | None = self
247-
while not isinstance(node, App):
248-
if node is None:
249-
raise NoActiveAppError()
250-
node = node._parent
252+
node: MessagePump | None = self
253+
while not isinstance(node, App):
254+
if node is None:
255+
raise NoActiveAppError()
256+
node = node._parent
251257

252-
return node
258+
return node
253259

254260
@property
255261
def is_attached(self) -> bool:

src/textual/widgets/_text_area.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,11 +1558,9 @@ def edit(self, edit: Edit) -> EditResult:
15581558

15591559
self._refresh_size()
15601560
edit.after(self)
1561-
15621561
self._build_highlight_map()
15631562
self.post_message(self.Changed(self))
15641563
self.update_suggestion()
1565-
15661564
return result
15671565

15681566
def undo(self) -> None:

tests/test_getters.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,19 @@ def compose(self) -> ComposeResult:
4343

4444
with pytest.raises(NoMatches):
4545
app.label2_missing
46+
47+
48+
async def test_app_getter() -> None:
49+
50+
class MyApp(App):
51+
def compose(self) -> ComposeResult:
52+
my_widget = MyWidget()
53+
my_widget.app
54+
yield my_widget
55+
56+
class MyWidget(Widget):
57+
app = getters.app(MyApp)
58+
59+
app = MyApp()
60+
async with app.run_test():
61+
assert isinstance(app.query_one(MyWidget).app, MyApp)

0 commit comments

Comments
 (0)