prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.26k stars 715 forks source link

with patch_stdout(): doesn't work with print_formatted_text #1172

Open GavanWilhite opened 4 years ago

GavanWilhite commented 4 years ago

Using print_formatted_text will clobber the prompt / not recreate it after each output.

If I swap print_formatted_text for print, it restores the prompt after each output.

Any suggestions to resolve this?

mattsta commented 4 years ago

This has been breaking my interface for weeks, so I decided to figure it out today.

It turns out patch_stdout() does what it says: it replaces stdout (and stderr) with a locking proxy to render text above the prompt (and without breaking the bottom toolbar if present).

but print_formatted_text() doesn't use the currently defined stdout at all (which is what patch_stdout() overrides). It appends directly to the vt100 buffer, and the vt100 interface expressly ignores the patch_stdout() hack:

    # If the patch_stdout context manager has been used, then sys.stdout is
    # replaced by this proxy. For prompt_toolkit applications, we want to use
    # the real stdout.
    while isinstance(stdout, StdoutProxy):
        stdout = stdout.original_stdout

So, at this point in time, patch_stdout() has zero effect on print_formatted_text() and the results will always be interleaved (my problem was: I'm running async logging tasks along with the prompt, and I have a bottom toolbar updating every few seconds, so the async logging printing to the screen was constantly tearing bottom toolbar fragments up the screen when they logged).

My fix for now is just using default python print() wrapped in patch_stdout() since all I needed was printed ANSI color stings anyway and passing regular ANSI escape sequences to print() works fine.

GavanWilhite commented 4 years ago

Thanks for posting.

For what it's worth, I was encountering this in the same situation and landed on the same solution

tchar commented 3 years ago

Hey, I have been digging into the code for the last couple of hours and apparently @mattsta is correct about the print_formatted_text() ignoring the stdout. The problem is not only that it ignores stdout but also that it runs outside of the event loop of prompt.

If you check the patch_stdout.py there is a line inside the StdoutProxy that calls loop.call_soon_threadsafe(..).

Apparenntly the print_formatted_text() is not implemented using this so whatever you print to the screen using that function it's going to ignore the loop and just print.

I wrote a small piece of code that patches this behavior and uses the same logic as in patch_stdout.py to call the print_formatted_text with checking the event loop.

Here it is:

from threading import main_thread, current_thread
from asyncio import get_event_loop
from prompt_toolkit.application import run_in_terminal
from prompt_toolkit import print_formatted_text as original_print_formatted_text

class _ThreadCtx():
    loop = get_event_loop()

def print_formatted_text(*args, **kwargs):

    loop = _ThreadCtx.loop

    def _print_formatted_text():
        original_print_formatted_text(*args, **kwargs)

    def _run_in_terminal():
        run_in_terminal(_print_formatted_text, in_executor=False)

    if main_thread() == current_thread():
        _print_formatted_text()
    else:
        loop.call_soon_threadsafe(_run_in_terminal)

You can use it like that:

from prompt_toolkit import PromptSession
from threading import Timer
from prompt_toolkit.formatted_text import HTML
from patch_formatted_text import print_formatted_text
import random

text = (
    '<style fg="#ff0066">thread {}</style> '
    '<style fg="#44ff44"><i>loop {}</i></style>'
)

def just_a_thread(i, j):
    print_formatted_text(HTML(text.format(i, j)))
    Timer(random.uniform(1, 2), just_a_thread, [i, j+1]).start()

Timer(random.uniform(1, 2), just_a_thread, [1, 1]).start()
Timer(random.uniform(1, 2), just_a_thread, [2, 1]).start()

print_formatted_text(HTML(text.format(0, 0)))
result = PromptSession().prompt("> ")

Now I am not sure if the main_loop is going to change. I need to check the source code to see what is going on.

Regardless of that the code above works just fine for me. So let me know if you try it, maybe we wrap it up prettify it and make a pull request.