Skip to content

Commit e32e094

Browse files
authored
Support callables in App.SCREENS (#1185)
* Support Type[Screen] in App.SCREENS (lazy screens) * Update CHANGELOG * Remove redundant isinstance
1 parent df37a9b commit e32e094

File tree

3 files changed

+39
-5
lines changed

3 files changed

+39
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2525
https://github.com/Textualize/textual/issues/1094
2626
- Added Pilot.wait_for_animation
2727
- Added `Widget.move_child` https://github.com/Textualize/textual/issues/1121
28+
- Support lazy-instantiated Screens (callables in App.SCREENS) https://github.com/Textualize/textual/pull/1185
2829

2930
### Changed
3031

src/textual/app.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
TypeVar,
2626
Union,
2727
cast,
28+
Callable,
2829
)
2930
from weakref import WeakSet, WeakValueDictionary
3031

@@ -228,7 +229,7 @@ class App(Generic[ReturnType], DOMNode):
228229
}
229230
"""
230231

231-
SCREENS: dict[str, Screen] = {}
232+
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
232233
_BASE_PATH: str | None = None
233234
CSS_PATH: CSSPathType = None
234235
TITLE: str | None = None
@@ -330,7 +331,7 @@ def __init__(
330331
self._registry: WeakSet[DOMNode] = WeakSet()
331332

332333
self._installed_screens: WeakValueDictionary[
333-
str, Screen
334+
str, Screen | Callable[[], Screen]
334335
] = WeakValueDictionary()
335336
self._installed_screens.update(**self.SCREENS)
336337

@@ -998,12 +999,15 @@ def get_screen(self, screen: Screen | str) -> Screen:
998999
next_screen = self._installed_screens[screen]
9991000
except KeyError:
10001001
raise KeyError(f"No screen called {screen!r} installed") from None
1002+
if callable(next_screen):
1003+
next_screen = next_screen()
1004+
self._installed_screens[screen] = next_screen
10011005
else:
10021006
next_screen = screen
10031007
return next_screen
10041008

10051009
def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
1006-
"""Get an installed screen and a await mount object.
1010+
"""Get an installed screen and an AwaitMount object.
10071011
10081012
If the screen isn't running, it will be registered before it is run.
10091013
@@ -1558,7 +1562,7 @@ async def _close_all(self) -> None:
15581562

15591563
# Close pre-defined screens
15601564
for screen in self.SCREENS.values():
1561-
if screen._running:
1565+
if isinstance(screen, Screen) and screen._running:
15621566
await self._prune_node(screen)
15631567

15641568
# Close any remaining nodes

tests/test_screens.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,37 @@
1111
)
1212

1313

14+
async def test_installed_screens():
15+
class ScreensApp(App):
16+
SCREENS = {
17+
"home": Screen, # Screen type
18+
"one": Screen(), # Screen instance
19+
"two": lambda: Screen() # Callable[[], Screen]
20+
}
21+
22+
app = ScreensApp()
23+
async with app.run_test() as pilot:
24+
pilot.app.push_screen("home") # Instantiates and pushes the "home" screen
25+
pilot.app.push_screen("one") # Pushes the pre-instantiated "one" screen
26+
pilot.app.push_screen("home") # Pushes the single instance of "home" screen
27+
pilot.app.push_screen("two") # Calls the callable, pushes returned Screen instance
28+
29+
assert len(app.screen_stack) == 5
30+
assert app.screen_stack[1] is app.screen_stack[3]
31+
assert app.screen is app.screen_stack[4]
32+
assert isinstance(app.screen, Screen)
33+
assert app.is_screen_installed(app.screen)
34+
35+
assert pilot.app.pop_screen()
36+
assert pilot.app.pop_screen()
37+
assert pilot.app.pop_screen()
38+
assert pilot.app.pop_screen()
39+
with pytest.raises(ScreenStackError):
40+
pilot.app.pop_screen()
41+
42+
43+
1444
@skip_py310
15-
@pytest.mark.asyncio
1645
async def test_screens():
1746

1847
app = App()

0 commit comments

Comments
 (0)