Skip to content

avoid exposing asyncio.Future directly to api consumers #5765

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
16 changes: 5 additions & 11 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
ScreenResultCallbackType,
ScreenResultType,
SystemModalScreen,
AwaitScreen,
)
from textual.signal import Signal
from textual.theme import BUILTIN_THEMES, Theme, ThemeProvider
Expand Down Expand Up @@ -2650,14 +2651,14 @@ def push_screen(
screen: Screen[ScreenResultType] | str,
callback: ScreenResultCallbackType[ScreenResultType] | None = None,
wait_for_dismiss: Literal[True] = True,
) -> asyncio.Future[ScreenResultType]: ...
) -> AwaitScreen[ScreenResultType]: ...

def push_screen(
self,
screen: Screen[ScreenResultType] | str,
callback: ScreenResultCallbackType[ScreenResultType] | None = None,
wait_for_dismiss: bool = False,
) -> AwaitMount | asyncio.Future[ScreenResultType]:
) -> AwaitMount | AwaitScreen[ScreenResultType]:
"""Push a new [screen](/guide/screens) on the screen stack, making it the current screen.

Args:
Expand All @@ -2670,22 +2671,14 @@ def push_screen(
NoActiveWorker: If using `wait_for_dismiss` outside of a worker.

Returns:
An optional awaitable that awaits the mounting of the screen and its children, or an asyncio Future
An optional awaitable that awaits the mounting of the screen and its children, or an awaitable
to await the result of the screen.
"""
if not isinstance(screen, (Screen, str)):
raise TypeError(
f"push_screen requires a Screen instance or str; not {screen!r}"
)

try:
loop = asyncio.get_running_loop()
except RuntimeError:
# Mainly for testing, when push_screen isn't called in an async context
future: asyncio.Future[ScreenResultType] = asyncio.Future()
else:
future = loop.create_future()

if self._screen_stack:
self.screen.post_message(events.ScreenSuspend())
self.screen.refresh()
Expand All @@ -2695,6 +2688,7 @@ def push_screen(
except LookupError:
message_pump = self.app

future: AwaitScreen[ScreenResultType] = AwaitScreen()
next_screen._push_result_callback(message_pump, callback, future)
self._load_screen_css(next_screen)
self._screen_stack.append(next_screen)
Expand Down
28 changes: 26 additions & 2 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import enum
import asyncio
from functools import partial
from operator import attrgetter
Expand All @@ -23,6 +24,8 @@
Optional,
TypeVar,
Union,
Literal,
Generator,
)

import rich.repr
Expand Down Expand Up @@ -82,6 +85,27 @@
]
"""Type of a screen result callback function."""

class _Unset(enum.Enum):
UNSET = enum.auto()

class AwaitScreen(Generic[ScreenResultType]):
def __init__(self) -> None:
self._event = asyncio.Event()
self._result: ScreenResultType | Literal[_Unset.UNSET] = _Unset.UNSET

async def wait(self) -> ScreenResultType:
await self._event.wait()
assert self._result is not _Unset.UNSET
return self._result

def __await__(self) -> Generator[Any, Any, ScreenResultType]:
return self.wait().__await__()

def set_result(self, result):
assert self._result is _Unset.UNSET
self._result = result
self._event.set()


@rich.repr.auto
class ResultCallback(Generic[ScreenResultType]):
Expand All @@ -91,7 +115,7 @@ def __init__(
self,
requester: MessagePump,
callback: ScreenResultCallbackType[ScreenResultType] | None,
future: asyncio.Future[ScreenResultType] | None = None,
future: AwaitScreen[ScreenResultType] | None = None,
) -> None:
"""Initialise the result callback object.

Expand Down Expand Up @@ -1161,7 +1185,7 @@ def _push_result_callback(
self,
requester: MessagePump,
callback: ScreenResultCallbackType[ScreenResultType] | None,
future: asyncio.Future[ScreenResultType | None] | None = None,
future: AwaitScreen[ScreenResultType] | None = None,
) -> None:
"""Add a result callback to the screen.

Expand Down