Skip to content

Commit bff8fbf

Browse files
authored
Merge pull request #4795 from Textualize/screen-result
allow None in Screen callback
2 parents 1c867b0 + 6af0026 commit bff8fbf

File tree

13 files changed

+97
-27
lines changed

13 files changed

+97
-27
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Fixed exception when removing Selects https://github.com/Textualize/textual/pull/4786
1414
- Fixed issue with non-clickable Footer keys https://github.com/Textualize/textual/pull/4798
1515

16+
### Changed
17+
18+
- Calling `Screen.dismiss` with no arguments will invoke the screen callback with `None` (previously the callback wasn't invoke at all). https://github.com/Textualize/textual/pull/4795
19+
1620
## [0.73.0] - 2024-07-18
1721

1822
### Added

docs/examples/guide/screens/modal03.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def compose(self) -> ComposeResult:
4444
def action_request_quit(self) -> None:
4545
"""Action to display the quit dialog."""
4646

47-
def check_quit(quit: bool) -> None:
47+
def check_quit(quit: bool | None) -> None:
4848
"""Called when QuitScreen is dismissed."""
4949
if quit:
5050
self.exit()

src/textual/_debug.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
Functions related to debugging.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from . import constants
8+
9+
10+
def get_caller_file_and_line() -> str | None:
11+
"""Get the caller filename and line, if in debug mode, otherwise return `None`:
12+
13+
Returns:
14+
Path and file if `constants.DEBUG==True`
15+
"""
16+
17+
if not constants.DEBUG:
18+
return None
19+
import inspect
20+
21+
try:
22+
current_frame = inspect.currentframe()
23+
caller_frame = inspect.getframeinfo(current_frame.f_back.f_back)
24+
return f"{caller_frame.filename}:{caller_frame.lineno}"
25+
except Exception:
26+
return None

src/textual/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3599,7 +3599,7 @@ def clear_notifications(self) -> None:
35993599
def action_command_palette(self) -> None:
36003600
"""Show the Textual command palette."""
36013601
if self.use_command_palette and not CommandPalette.is_open(self):
3602-
self.push_screen(CommandPalette(), callback=self.call_next)
3602+
self.push_screen(CommandPalette())
36033603

36043604
def _suspend_signal(self) -> None:
36053605
"""Signal that the application is being suspended."""

src/textual/await_complete.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import rich.repr
77
from typing_extensions import Self
88

9+
from ._debug import get_caller_file_and_line
910
from .message_pump import MessagePump
1011

1112
if TYPE_CHECKING:
@@ -27,10 +28,12 @@ def __init__(
2728
self._awaitables = awaitables
2829
self._future: Future[Any] = gather(*awaitables)
2930
self._pre_await: CallbackType | None = pre_await
31+
self._caller = get_caller_file_and_line()
3032

3133
def __rich_repr__(self) -> rich.repr.Result:
3234
yield self._awaitables
3335
yield "pre_await", self._pre_await, None
36+
yield "caller", self._caller, None
3437

3538
def set_pre_await_callback(self, pre_await: CallbackType | None) -> None:
3639
"""Set a callback to run prior to awaiting.

src/textual/await_remove.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
from asyncio import Task, gather
99
from typing import Generator
1010

11+
import rich.repr
12+
1113
from ._callback import invoke
14+
from ._debug import get_caller_file_and_line
1215
from ._types import CallbackType
1316

1417

18+
@rich.repr.auto
1519
class AwaitRemove:
1620
"""An awaitable that waits for nodes to be removed."""
1721

@@ -20,6 +24,12 @@ def __init__(
2024
) -> None:
2125
self._tasks = tasks
2226
self._post_remove = post_remove
27+
self._caller = get_caller_file_and_line()
28+
29+
def __rich_repr__(self) -> rich.repr.Result:
30+
yield "tasks", self._tasks
31+
yield "post_remove", self._post_remove
32+
yield "caller", self._caller, None
2333

2434
async def __call__(self) -> None:
2535
await self

src/textual/command.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from .reactive import var
4040
from .screen import Screen, SystemModalScreen
4141
from .timer import Timer
42-
from .types import CallbackType, IgnoreReturnCallbackType
42+
from .types import IgnoreReturnCallbackType
4343
from .widget import Widget
4444
from .widgets import Button, Input, LoadingIndicator, OptionList, Static
4545
from .widgets.option_list import Option
@@ -419,7 +419,7 @@ class CommandInput(Input):
419419
"""
420420

421421

422-
class CommandPalette(SystemModalScreen[CallbackType]):
422+
class CommandPalette(SystemModalScreen):
423423
"""The Textual command palette."""
424424

425425
AUTO_FOCUS = "CommandInput"
@@ -1079,7 +1079,8 @@ def _select_or_command(
10791079
# decide what to do with it (hopefully it'll run it).
10801080
self._cancel_gather_commands()
10811081
self.app.post_message(CommandPalette.Closed(option_selected=True))
1082-
self.dismiss(self._selected_command.command)
1082+
self.dismiss()
1083+
self.call_later(self._selected_command.command)
10831084

10841085
@on(OptionList.OptionHighlighted)
10851086
def _stop_event_leak(self, event: OptionList.OptionHighlighted) -> None:

src/textual/message_pump.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ def call_next(self, callback: Callback, *args: Any, **kwargs: Any) -> None:
446446
*args: Positional arguments to pass to the callable.
447447
**kwargs: Keyword arguments to pass to the callable.
448448
"""
449+
assert callback is not None, "Callback must not be None"
449450
callback_message = events.Callback(callback=partial(callback, *args, **kwargs))
450451
callback_message._prevent.update(self._get_prevented_messages())
451452
self._next_callbacks.append(callback_message)

src/textual/screen.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@
2020
Generic,
2121
Iterable,
2222
Iterator,
23-
Type,
23+
Optional,
2424
TypeVar,
2525
Union,
26-
cast,
2726
)
2827

2928
import rich.repr
@@ -68,7 +67,8 @@
6867
"""The result type of a screen."""
6968

7069
ScreenResultCallbackType = Union[
71-
Callable[[ScreenResultType], None], Callable[[ScreenResultType], Awaitable[None]]
70+
Callable[[Optional[ScreenResultType]], None],
71+
Callable[[Optional[ScreenResultType]], Awaitable[None]],
7272
]
7373
"""Type of a screen result callback function."""
7474

@@ -110,6 +110,7 @@ def __call__(self, result: ScreenResultType) -> None:
110110
self.future.set_result(result)
111111
if self.requester is not None and self.callback is not None:
112112
self.requester.call_next(self.callback, result)
113+
self.callback = None
113114

114115

115116
@rich.repr.auto
@@ -209,7 +210,7 @@ def __init__(
209210
self._dirty_widgets: set[Widget] = set()
210211
self.__update_timer: Timer | None = None
211212
self._callbacks: list[tuple[CallbackType, MessagePump]] = []
212-
self._result_callbacks: list[ResultCallback[ScreenResultType]] = []
213+
self._result_callbacks: list[ResultCallback[ScreenResultType | None]] = []
213214

214215
self._tooltip_widget: Widget | None = None
215216
self._tooltip_timer: Timer | None = None
@@ -884,7 +885,7 @@ def _push_result_callback(
884885
self,
885886
requester: MessagePump,
886887
callback: ScreenResultCallbackType[ScreenResultType] | None,
887-
future: asyncio.Future[ScreenResultType] | None = None,
888+
future: asyncio.Future[ScreenResultType | None] | None = None,
888889
) -> None:
889890
"""Add a result callback to the screen.
890891
@@ -894,7 +895,7 @@ def _push_result_callback(
894895
future: A Future to hold the result.
895896
"""
896897
self._result_callbacks.append(
897-
ResultCallback[ScreenResultType](requester, callback, future)
898+
ResultCallback[Optional[ScreenResultType]](requester, callback, future)
898899
)
899900

900901
def _pop_result_callback(self) -> None:
@@ -1227,14 +1228,11 @@ def _forward_event(self, event: events.Event) -> None:
12271228
else:
12281229
self.post_message(event)
12291230

1230-
class _NoResult:
1231-
"""Class used to mark that there is no result."""
1232-
1233-
def dismiss(
1234-
self, result: ScreenResultType | Type[_NoResult] = _NoResult
1235-
) -> AwaitComplete:
1231+
def dismiss(self, result: ScreenResultType | None = None) -> AwaitComplete:
12361232
"""Dismiss the screen, optionally with a result.
12371233
1234+
Any callback provided in [push_screen][textual.app.push_screen] will be invoked with the supplied result.
1235+
12381236
Only the active screen may be dismissed. This method will produce a warning in the logs if
12391237
called on an inactive screen (but otherwise have no effect).
12401238
@@ -1244,9 +1242,6 @@ def dismiss(
12441242
message handler on the Screen being dismissed. If you want to dismiss the current screen, you can
12451243
call `self.dismiss()` _without_ awaiting.
12461244
1247-
If `result` is provided and a callback was set when the screen was [pushed][textual.app.App.push_screen], then
1248-
the callback will be invoked with `result`.
1249-
12501245
Args:
12511246
result: The optional result to be passed to the result callback.
12521247
@@ -1255,8 +1250,9 @@ def dismiss(
12551250
if not self.is_active:
12561251
self.log.warning("Can't dismiss inactive screen")
12571252
return AwaitComplete()
1258-
if result is not self._NoResult and self._result_callbacks:
1259-
self._result_callbacks[-1](cast(ScreenResultType, result))
1253+
if self._result_callbacks:
1254+
callback = self._result_callbacks[-1]
1255+
callback(result)
12601256
await_pop = self.app.pop_screen()
12611257

12621258
def pre_await() -> None:
@@ -1273,9 +1269,7 @@ def pre_await() -> None:
12731269

12741270
return await_pop
12751271

1276-
async def action_dismiss(
1277-
self, result: ScreenResultType | Type[_NoResult] = _NoResult
1278-
) -> None:
1272+
async def action_dismiss(self, result: ScreenResultType | None = None) -> None:
12791273
"""A wrapper around [`dismiss`][textual.screen.Screen.dismiss] that can be called as an action.
12801274
12811275
Args:

src/textual/widget.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from ._arrange import DockArrangeResult, arrange
5151
from ._compose import compose
5252
from ._context import NoActiveAppError, active_app
53+
from ._debug import get_caller_file_and_line
5354
from ._dispatch_key import dispatch_key
5455
from ._easing import DEFAULT_SCROLL_EASING
5556
from ._layout import Layout
@@ -114,6 +115,7 @@
114115
_MOUSE_EVENTS_ALLOW_IF_DISABLED = (events.MouseScrollDown, events.MouseScrollUp)
115116

116117

118+
@rich.repr.auto
117119
class AwaitMount:
118120
"""An *optional* awaitable returned by [mount][textual.widget.Widget.mount] and [mount_all][textual.widget.Widget.mount_all].
119121
@@ -126,6 +128,12 @@ class AwaitMount:
126128
def __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None:
127129
self._parent = parent
128130
self._widgets = widgets
131+
self._caller = get_caller_file_and_line()
132+
133+
def __rich_repr__(self) -> rich.repr.Result:
134+
yield "parent", self._parent
135+
yield "widgets", self._widgets
136+
yield "caller", self._caller, None
129137

130138
async def __call__(self) -> None:
131139
"""Allows awaiting via a call operation."""

0 commit comments

Comments
 (0)