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 3 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
14 changes: 9 additions & 5 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 @@ -979,12 +980,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 @@ -1532,14 +1536,14 @@ async def _close_all(self) -> None:

# Close all screens on the stack
for screen in self._screen_stack:
if screen._running:
if isinstance(screen, Screen) and screen._running:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to have something on the Screen stack that is not a screen?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, good catch. Have removed this isinstance check

await self._prune_node(screen)

self._screen_stack.clear()

# 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