2025, Nov 01 21:16

Как эмулировать REPL для Python‑скрипта с InteractiveConsole

Пошагово показываем, как эмулировать REPL для Python‑скрипта с помощью code.InteractiveConsole: подсказки >>> и ..., вывод выражений и перехват stdout, для демо.

Запуск Python‑файла редко напоминает полноценную REPL‑сессию. В режиме выполнения скрипта интерпретатор не показывает приглашения, не выводит значения «голых» выражений и нарушает интерактивный ритм, на который полагаются многие разработчики и инструменты. Бывает, нужно обратное: передать файл Python и получить вывод, который в точности имитирует ручную интерактивную сессию — с подсказками >>> и ... и отображением результатов выражений.

Пример входных данных, которые должны вести себя как в REPL

Представьте обычный Python‑файл, который нужно «проиграть» так, будто вы вводили его построчно в интерактивной оболочке:

n = 5
n + 8
if n < 4:
    print("123")
else:
    print("xyz")
exit()

Что на самом деле происходит и почему это непросто

У интерпретатора есть два разных режима. В интерактивном (REPL) он показывает приглашения, вычисляет и отображает значения «голых» выражений и обрабатывает многострочные конструкции с продолжением приглашения. В режиме скрипта он просто выполняет файл — без приглашений и без вывода результатов выражений. Обычные опции командной строки не превращают скрипт в полноценную запись REPL‑сессии.

Есть прямой способ программно смоделировать интерактивное поведение. Модуль code предоставляет InteractiveConsole, которая принимает код построчно и оценивает его так же, как это делает REPL. Подав файл в эту консоль, выводя приглашения самостоятельно и перехватывая stdout, вы получите нужную «расшифровку» сессии. Если вы задумывались, делает ли что‑то похожее doctest: он запускает примеры через exec(compile(example.source)), то есть это не генератор полноценной REPL‑транскрипции.

Решение: управлять code.InteractiveConsole и перехватывать stdout

Подход такой: прочитать файл, построчно передавать его в InteractiveConsole, самостоятельно выводить подсказки >>> и ..., и перехватывать всё, что печатает интерпретатор. Приведённый ниже фрагмент также понимает строки, начинающиеся с ..., поэтому вы можете добавить в исходник маркеры продолжения, если хотите отразить формат интерактивного ввода для многострочных блоков.

import code
import sys
import io
from contextlib import redirect_stdout
def emulate_repl_session(path_to_file):
    # Прочитать файл в память
    with open(path_to_file, 'r') as fh:
        rows = fh.readlines()
    # Нормализовать переводы строк и сделать обработку пустых строк предсказуемой
    rows = [r.rstrip('\n') for r in rows]
    # Баннер REPL (можно выводить или пропустить)
    print(f"Python {sys.version} on {sys.platform}")
    print('Type "help", "copyright", "credits" or "license" for more information.')
    # Создать интерактивную консоль
    interp = code.InteractiveConsole()
    # Буферы и состояние для многострочного ввода
    chunk = []
    awaiting = False
    for row in rows:
        # Игнорировать пустые строки, как в обычном вводе REPL
        if not row.strip():
            continue
        # Строки продолжения, начинающиеся с '...'
        if row.strip().startswith('...'):
            chunk.append(row.replace('...', '', 1).lstrip())
            continue
        # Если мы набирали многострочный блок, вывести и выполнить его
        if awaiting:
            suite = '\n'.join(chunk)
            print(f'>>> {chunk[0]}')
            for frag in chunk[1:]:
                print(f'... {frag}')
            with io.StringIO() as stream, redirect_stdout(stream):
                cont = interp.push(suite)
                emitted = stream.getvalue()
            if emitted:
                print(emitted, end='')
            chunk = []
            awaiting = cont
            if row.strip() == 'exit()':
                break
            if not awaiting:
                continue
        # Обработать явный выход из сессии
        if row.strip() == 'exit()':
            print('>>> exit()')
            break
        # Вывести приглашение и введённую строку
        print(f'>>> {row}')
        # Начать сбор блока, если строка открывает составную инструкцию
        if row.rstrip().endswith(':'):
            chunk.append(row)
            awaiting = True
            continue
        # Выполнить одну строку и перехватить её вывод
        with io.StringIO() as stream, redirect_stdout(stream):
            cont = interp.push(row)
            emitted = stream.getvalue()
        if emitted:
            print(emitted, end='')
        awaiting = cont
if __name__ == '__main__':
    if len(sys.argv) != 2:
        print('Usage: python repl_emulator.py <script_path>')
        sys.exit(1)
    emulate_repl_session(sys.argv[1])

Почему это работает

InteractiveConsole выполняет фрагменты кода так же, как REPL, включая семантику вычислений, из‑за которой выражения дают видимый вывод. Управляя приглашениями и отправляя каждый фрагмент через push, вы воссоздаёте пользовательскую транскрипцию. stdout перехватывается через contextlib.redirect_stdout и затем выводится без изменений, поэтому всё, что пишет интерпретатор, оказывается именно там, где человек ожидал бы увидеть это в живой сессии.

Зачем это знать

Воспроизводимые «транскрипты» REPL упрощают автоматизацию, демонстрации и проверку. Вместо копирования из живого терминала можно хранить обычный .py‑файл и при необходимости заново получать тот же интерактивный на вид вывод — ясно и стабильно. Если во входных данных уже есть строки продолжения, начинающиеся с ..., ход выполнения остаётся естественным и читаемым, при этом оставаясь детерминированным.

Итоги

Когда нужно, чтобы выполнение скрипта выглядело как настоящая интерактивная сессия, опирайтесь на code.InteractiveConsole и небольшой вспомогательный код вокруг неё. Подавайте файл построчно, выводите приглашения, перехватывайте stdout — остальное сделает интерпретатор. Держите исходник простым: используйте обычные операторы Python вместо заранее вставленных маркеров >>> и прибегайте к ... только тогда, когда намеренно отражаете многострочный ввод. Так вы получите достоверную запись в стиле REPL без борьбы с флагами командной строки и без переизобретения семантики вычислений.

Статья основана на вопросе на StackOverflow от Thomas Weise и ответе от Tushar Neupaney.