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.