Skip to content

Commit eeaac8e

Browse files
committed
Refactoring of StdoutProxy to take app_session from the caller
This changes makes StdoutProxy to take the exact app_session from by taking it as optional argument in the constructor instead finding it by calling get_app_session() globally. It can avoid confliction of printing output within the patch_stdout context in the ssh session when the multiple ssh connections are performing concurrently. The changed StdoutProxy now passes the correct app instance from the given app_session in the constructor to the run_in_terminal() in it instead of calling get_app_or_none() globally that can give wrong app instance from the prompt session in the last ssh connection.
1 parent 465ab02 commit eeaac8e

File tree

3 files changed

+44
-7
lines changed

3 files changed

+44
-7
lines changed

Diff for: examples/ssh/asyncssh-server.py

+24
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from prompt_toolkit.completion import WordCompleter
1212
from prompt_toolkit.contrib.ssh import PromptToolkitSSHServer, PromptToolkitSSHSession
1313
from prompt_toolkit.lexers import PygmentsLexer
14+
from prompt_toolkit.patch_stdout import StdoutProxy
1415
from prompt_toolkit.shortcuts import ProgressBar, print_formatted_text
1516
from prompt_toolkit.shortcuts.dialogs import input_dialog, yes_no_dialog
1617
from prompt_toolkit.shortcuts.prompt import PromptSession
@@ -99,6 +100,29 @@ async def interact(ssh_session: PromptToolkitSSHSession) -> None:
99100
await prompt_session.prompt_async("Showing input dialog... [ENTER]")
100101
await input_dialog("Input dialog", "Running over asyncssh").run_async()
101102

103+
async def print_counter(output):
104+
"""
105+
Coroutine that prints counters.
106+
"""
107+
try:
108+
i = 0
109+
while True:
110+
output.write(f"Counter: {i}\n")
111+
i += 1
112+
await asyncio.sleep(3)
113+
except asyncio.CancelledError:
114+
print("Background task cancelled.")
115+
116+
with StdoutProxy(app_session=prompt_session) as output:
117+
background_task = asyncio.create_task(print_counter(output))
118+
try:
119+
text = await prompt_session.prompt_async(
120+
"Type something with background task: "
121+
)
122+
output.write(f"You typed: {text}\n")
123+
finally:
124+
background_task.cancel()
125+
102126

103127
async def main(port=8222):
104128
# Set up logging.

Diff for: src/prompt_toolkit/application/run_in_terminal.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121

2222
def run_in_terminal(
23-
func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False
23+
func: Callable[[], _T],
24+
render_cli_done: bool = False,
25+
in_executor: bool = False,
26+
app=None,
2427
) -> Awaitable[_T]:
2528
"""
2629
Run function on the terminal above the current application or prompt.
@@ -40,12 +43,13 @@ def run_in_terminal(
4043
erase the interface first.
4144
:param in_executor: When True, run in executor. (Use this for long
4245
blocking functions, when you don't want to block the event loop.)
46+
:param app: instance of Application. (default None)
4347
4448
:returns: A `Future`.
4549
"""
4650

4751
async def run() -> _T:
48-
async with in_terminal(render_cli_done=render_cli_done):
52+
async with in_terminal(render_cli_done=render_cli_done, app=app):
4953
if in_executor:
5054
return await run_in_executor_with_context(func)
5155
else:
@@ -55,7 +59,9 @@ async def run() -> _T:
5559

5660

5761
@asynccontextmanager
58-
async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]:
62+
async def in_terminal(
63+
render_cli_done: bool = False, app=None
64+
) -> AsyncGenerator[None, None]:
5965
"""
6066
Asynchronous context manager that suspends the current application and runs
6167
the body in the terminal.
@@ -67,7 +73,8 @@ async def f():
6773
call_some_function()
6874
await call_some_async_function()
6975
"""
70-
app = get_app_or_none()
76+
if not app:
77+
app = get_app_or_none()
7178
if app is None or not app._is_running:
7279
yield
7380
return

Diff for: src/prompt_toolkit/patch_stdout.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525
import threading
2626
import time
2727
from contextlib import contextmanager
28-
from typing import Generator, TextIO, cast
28+
from typing import Any, Generator, TextIO, cast
2929

3030
from .application import get_app_session, run_in_terminal
3131
from .output import Output
32+
from .shortcuts.prompt import PromptSession
3233

3334
__all__ = [
3435
"patch_stdout",
@@ -95,6 +96,7 @@ def __init__(
9596
self,
9697
sleep_between_writes: float = 0.2,
9798
raw: bool = False,
99+
app_session: PromptSession[Any] | None = None,
98100
) -> None:
99101
self.sleep_between_writes = sleep_between_writes
100102
self.raw = raw
@@ -103,7 +105,9 @@ def __init__(
103105
self._buffer: list[str] = []
104106

105107
# Keep track of the curret app session.
106-
self.app_session = get_app_session()
108+
self.app_session = app_session
109+
if not self.app_session:
110+
self.app_session = get_app_session()
107111

108112
# See what output is active *right now*. We should do it at this point,
109113
# before this `StdoutProxy` instance is possibly assigned to `sys.stdout`.
@@ -220,7 +224,9 @@ def write_and_flush() -> None:
220224
def write_and_flush_in_loop() -> None:
221225
# If an application is running, use `run_in_terminal`, otherwise
222226
# call it directly.
223-
run_in_terminal(write_and_flush, in_executor=False)
227+
run_in_terminal(
228+
write_and_flush, in_executor=False, app=self.app_session.app
229+
)
224230

225231
if loop is None:
226232
# No loop, write immediately.

0 commit comments

Comments
 (0)