Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support callables in App.SCREENS #1185

Merged
merged 5 commits into from
Nov 16, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
https://github.com/Textualize/textual/issues/1094
- Added Pilot.wait_for_animation
- 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

Expand Down
12 changes: 8 additions & 4 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
TypeVar,
Union,
cast,
Callable,
)
from weakref import WeakSet, WeakValueDictionary

Expand Down Expand Up @@ -228,7 +229,7 @@ class App(Generic[ReturnType], DOMNode):
}
"""

SCREENS: dict[str, Screen] = {}
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
_BASE_PATH: str | None = None
CSS_PATH: CSSPathType = None
TITLE: str | None = None
Expand Down Expand Up @@ -330,7 +331,7 @@ def __init__(
self._registry: WeakSet[DOMNode] = WeakSet()

self._installed_screens: WeakValueDictionary[
str, Screen
str, Screen | Callable[[], Screen]
] = WeakValueDictionary()
self._installed_screens.update(**self.SCREENS)

Expand Down Expand Up @@ -998,12 +999,15 @@ def get_screen(self, screen: Screen | str) -> Screen:
next_screen = self._installed_screens[screen]
except KeyError:
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:
next_screen = screen
return next_screen

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.

Expand Down Expand Up @@ -1558,7 +1562,7 @@ async def _close_all(self) -> None:

# Close pre-defined screens
for screen in self.SCREENS.values():
if screen._running:
if isinstance(screen, Screen) and screen._running:
await self._prune_node(screen)

# Close any remaining nodes
Expand Down
31 changes: 30 additions & 1 deletion tests/test_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,37 @@
)


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
@pytest.mark.asyncio
async def test_screens():

app = App()
Expand Down