From 8469def61324feb5e278f88bebad36af7a0d997e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 23:38:31 -0300 Subject: A send to Python's REPL runs once, the Enter after the paste, not in it A one-line send (print(1)) to Python 3.14 stopped at ... and needed another Enter: its REPL takes a bracketed paste, and an Enter read in the same read as the paste is part of the paste, one line or not (checked in a real pty). Into a REPL that asked for bracketed paste, the Enters (two for an open block) now wait for its next output, its answer to the paste, or 250 ms with nothing. fs-test runs python3 in a real pty and checks a one-line exec prints at once; the docs' pty/data recipe says to send the Enter apart. Co-Authored-By: Claude Opus 5.5 --- test/fs.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) (limited to 'test') diff --git a/test/fs.py b/test/fs.py index 890e75f6..81989f88 100644 --- a/test/fs.py +++ b/test/fs.py @@ -3,6 +3,7 @@ import contextlib import fcntl import json import os +import shutil import signal from pathlib import Path import subprocess @@ -466,6 +467,51 @@ def unattended_messages_expire(binary): assert not shown(), 'an unattended message outlived MessageLinger' +def python_repl_runs_one_line(binary): + """A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks + for bracketed paste) runs at once, with no second Enter: the Enter + reaches it after the paste, not inside it. A real pty, a real python3.""" + python = shutil.which('python3') + if python is None: + return + version = subprocess.run([python, '-c', 'import sys; print(sys.version_info >= (3, 13))'], + capture_output=True, text=True).stdout.strip() + if version != 'True': + return + with tempfile.TemporaryDirectory(prefix='pardes-repl-') as directory: + with session(binary, Path(directory), 'repl') as (client, _): + def panes(client): + return {int(row.split()[0]) for row in client.read('/index').splitlines()} + before = panes(client) + client.write('/pane/1/ctl', b'Tty\n') + deadline = time.monotonic() + 10 + while time.monotonic() < deadline and panes(client) == before: + time.sleep(.05) + tty = max(panes(client) - before) + def body(): + return client.read(f'/pane/{tty}/body').decode('utf-8', 'replace') + def wait_for(text, seconds=15): + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + if text in body(): + return True + time.sleep(.1) + return False + assert wait_for('$') or wait_for('>'), body() + client.write(f'/pane/{tty}/pty/data', python.encode() + b' -q\r') + assert wait_for('>>>'), body() + client.write(f'/pane/{tty}/ctl', b'Repl python\n') + # An exec on the REPL's own body goes to the REPL, as its event + # record written back does: an empty range acts on its text. + fid = client.open(f'/pane/{tty}/event', 2) + try: + record = b'MX0 0 0 10 print(6*7)\n' + client.rpc(118, struct.pack('