Support callables in App.SCREENS (#1185)

* Support Type[Screen] in App.SCREENS (lazy screens)

* Update CHANGELOG

* Remove redundant isinstance
This commit is contained in:
darrenburns
2022-11-16 15:47:48 +00:00
committed by GitHub
parent df37a9b90a
commit e32e094b92
3 changed files with 39 additions and 5 deletions

View File

@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
https://github.com/Textualize/textual/issues/1094 https://github.com/Textualize/textual/issues/1094
- Added Pilot.wait_for_animation - Added Pilot.wait_for_animation
- Added `Widget.move_child` https://github.com/Textualize/textual/issues/1121 - Added `Widget.move_child` https://github.com/Textualize/textual/issues/1121
- Support lazy-instantiated Screens (callables in App.SCREENS) https://github.com/Textualize/textual/pull/1185
### Changed ### Changed

View File

@@ -25,6 +25,7 @@ from typing import (
TypeVar, TypeVar,
Union, Union,
cast, cast,
Callable,
) )
from weakref import WeakSet, WeakValueDictionary from weakref import WeakSet, WeakValueDictionary
@@ -228,7 +229,7 @@ class App(Generic[ReturnType], DOMNode):
} }
""" """
SCREENS: dict[str, Screen] = {} SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
_BASE_PATH: str | None = None _BASE_PATH: str | None = None
CSS_PATH: CSSPathType = None CSS_PATH: CSSPathType = None
TITLE: str | None = None TITLE: str | None = None
@@ -330,7 +331,7 @@ class App(Generic[ReturnType], DOMNode):
self._registry: WeakSet[DOMNode] = WeakSet() self._registry: WeakSet[DOMNode] = WeakSet()
self._installed_screens: WeakValueDictionary[ self._installed_screens: WeakValueDictionary[
str, Screen str, Screen | Callable[[], Screen]
] = WeakValueDictionary() ] = WeakValueDictionary()
self._installed_screens.update(**self.SCREENS) self._installed_screens.update(**self.SCREENS)
@@ -998,12 +999,15 @@ class App(Generic[ReturnType], DOMNode):
next_screen = self._installed_screens[screen] next_screen = self._installed_screens[screen]
except KeyError: except KeyError:
raise KeyError(f"No screen called {screen!r} installed") from None raise KeyError(f"No screen called {screen!r} installed") from None
if callable(next_screen):
next_screen = next_screen()
self._installed_screens[screen] = next_screen
else: else:
next_screen = screen next_screen = screen
return next_screen return next_screen
def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]: def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
"""Get an installed screen and a await mount object. """Get an installed screen and an AwaitMount object.
If the screen isn't running, it will be registered before it is run. If the screen isn't running, it will be registered before it is run.
@@ -1558,7 +1562,7 @@ class App(Generic[ReturnType], DOMNode):
# Close pre-defined screens # Close pre-defined screens
for screen in self.SCREENS.values(): for screen in self.SCREENS.values():
if screen._running: if isinstance(screen, Screen) and screen._running:
await self._prune_node(screen) await self._prune_node(screen)
# Close any remaining nodes # Close any remaining nodes

View File

@@ -11,8 +11,37 @@ skip_py310 = pytest.mark.skipif(
) )
async def test_installed_screens():
class ScreensApp(App):
SCREENS = {
"home": Screen, # Screen type
"one": Screen(), # Screen instance
"two": lambda: Screen() # Callable[[], Screen]
}
app = ScreensApp()
async with app.run_test() as pilot:
pilot.app.push_screen("home") # Instantiates and pushes the "home" screen
pilot.app.push_screen("one") # Pushes the pre-instantiated "one" screen
pilot.app.push_screen("home") # Pushes the single instance of "home" screen
pilot.app.push_screen("two") # Calls the callable, pushes returned Screen instance
assert len(app.screen_stack) == 5
assert app.screen_stack[1] is app.screen_stack[3]
assert app.screen is app.screen_stack[4]
assert isinstance(app.screen, Screen)
assert app.is_screen_installed(app.screen)
assert pilot.app.pop_screen()
assert pilot.app.pop_screen()
assert pilot.app.pop_screen()
assert pilot.app.pop_screen()
with pytest.raises(ScreenStackError):
pilot.app.pop_screen()
@skip_py310 @skip_py310
@pytest.mark.asyncio
async def test_screens(): async def test_screens():
app = App() app = App()