Skip to content

Commit 9435124

Browse files
lysnikolaouwiggin15ambv
authored
[3.13] gh-111201: auto-indentation in _pyrepl (GH-119348) (#119427)
(cherry picked from commit cd516cd) Co-authored-by: Arnon Yaari <[email protected]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent 81440c5 commit 9435124

File tree

2 files changed

+179
-58
lines changed

2 files changed

+179
-58
lines changed

Lib/_pyrepl/readline.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
9999
# Instance fields
100100
config: ReadlineConfig
101101
more_lines: MoreLinesCallable | None = None
102+
last_used_indentation: str | None = None
102103

103104
def __post_init__(self) -> None:
104105
super().__post_init__()
@@ -157,6 +158,11 @@ def get_trimmed_history(self, maxlength: int) -> list[str]:
157158
cut = 0
158159
return self.history[cut:]
159160

161+
def update_last_used_indentation(self) -> None:
162+
indentation = _get_first_indentation(self.buffer)
163+
if indentation is not None:
164+
self.last_used_indentation = indentation
165+
160166
# --- simplified support for reading multiline Python statements ---
161167

162168
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
@@ -211,6 +217,28 @@ def _get_previous_line_indent(buffer: list[str], pos: int) -> tuple[int, int | N
211217
return prevlinestart, indent
212218

213219

220+
def _get_first_indentation(buffer: list[str]) -> str | None:
221+
indented_line_start = None
222+
for i in range(len(buffer)):
223+
if (i < len(buffer) - 1
224+
and buffer[i] == "\n"
225+
and buffer[i + 1] in " \t"
226+
):
227+
indented_line_start = i + 1
228+
elif indented_line_start is not None and buffer[i] not in " \t\n":
229+
return ''.join(buffer[indented_line_start : i])
230+
return None
231+
232+
233+
def _is_last_char_colon(buffer: list[str]) -> bool:
234+
i = len(buffer)
235+
while i > 0:
236+
i -= 1
237+
if buffer[i] not in " \t\n": # ignore whitespaces
238+
return buffer[i] == ":"
239+
return False
240+
241+
214242
class maybe_accept(commands.Command):
215243
def do(self) -> None:
216244
r: ReadlineAlikeReader
@@ -227,9 +255,18 @@ def do(self) -> None:
227255
# auto-indent the next line like the previous line
228256
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
229257
r.insert("\n")
230-
if not self.reader.paste_mode and indent:
231-
for i in range(prevlinestart, prevlinestart + indent):
232-
r.insert(r.buffer[i])
258+
if not self.reader.paste_mode:
259+
if indent:
260+
for i in range(prevlinestart, prevlinestart + indent):
261+
r.insert(r.buffer[i])
262+
r.update_last_used_indentation()
263+
if _is_last_char_colon(r.buffer):
264+
if r.last_used_indentation is not None:
265+
indentation = r.last_used_indentation
266+
else:
267+
# default
268+
indentation = " " * 4
269+
r.insert(indentation)
233270
elif not self.reader.paste_mode:
234271
self.finish = True
235272
else:

Lib/test/test_pyrepl/test_pyrepl.py

+139-55
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,31 @@
55
from unittest import TestCase
66
from unittest.mock import patch
77

8-
from .support import FakeConsole, handle_all_events, handle_events_narrow_console
9-
from .support import more_lines, multiline_input, code_to_events
8+
from .support import (
9+
FakeConsole,
10+
handle_all_events,
11+
handle_events_narrow_console,
12+
more_lines,
13+
multiline_input,
14+
code_to_events,
15+
)
1016
from _pyrepl.console import Event
1117
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
1218
from _pyrepl.readline import multiline_input as readline_multiline_input
1319

1420

1521
class TestCursorPosition(TestCase):
22+
def prepare_reader(self, events):
23+
console = FakeConsole(events)
24+
config = ReadlineConfig(readline_completer=None)
25+
reader = ReadlineAlikeReader(console=console, config=config)
26+
return reader
27+
1628
def test_up_arrow_simple(self):
1729
# fmt: off
1830
code = (
19-
'def f():\n'
20-
' ...\n'
31+
"def f():\n"
32+
" ...\n"
2133
)
2234
# fmt: on
2335
events = itertools.chain(
@@ -34,8 +46,8 @@ def test_up_arrow_simple(self):
3446
def test_down_arrow_end_of_input(self):
3547
# fmt: off
3648
code = (
37-
'def f():\n'
38-
' ...\n'
49+
"def f():\n"
50+
" ...\n"
3951
)
4052
# fmt: on
4153
events = itertools.chain(
@@ -300,6 +312,79 @@ def test_cursor_position_after_wrap_and_move_up(self):
300312
self.assertEqual(reader.pos, 10)
301313
self.assertEqual(reader.cxy, (1, 1))
302314

315+
def test_auto_indent_default(self):
316+
# fmt: off
317+
input_code = (
318+
"def f():\n"
319+
"pass\n\n"
320+
)
321+
322+
output_code = (
323+
"def f():\n"
324+
" pass\n"
325+
" "
326+
)
327+
# fmt: on
328+
329+
def test_auto_indent_continuation(self):
330+
# auto indenting according to previous user indentation
331+
# fmt: off
332+
events = itertools.chain(
333+
code_to_events("def f():\n"),
334+
# add backspace to delete default auto-indent
335+
[
336+
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
337+
],
338+
code_to_events(
339+
" pass\n"
340+
"pass\n\n"
341+
),
342+
)
343+
344+
output_code = (
345+
"def f():\n"
346+
" pass\n"
347+
" pass\n"
348+
" "
349+
)
350+
# fmt: on
351+
352+
reader = self.prepare_reader(events)
353+
output = multiline_input(reader)
354+
self.assertEqual(output, output_code)
355+
356+
def test_auto_indent_prev_block(self):
357+
# auto indenting according to indentation in different block
358+
# fmt: off
359+
events = itertools.chain(
360+
code_to_events("def f():\n"),
361+
# add backspace to delete default auto-indent
362+
[
363+
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
364+
],
365+
code_to_events(
366+
" pass\n"
367+
"pass\n\n"
368+
),
369+
code_to_events(
370+
"def g():\n"
371+
"pass\n\n"
372+
),
373+
)
374+
375+
376+
output_code = (
377+
"def g():\n"
378+
" pass\n"
379+
" "
380+
)
381+
# fmt: on
382+
383+
reader = self.prepare_reader(events)
384+
output1 = multiline_input(reader)
385+
output2 = multiline_input(reader)
386+
self.assertEqual(output2, output_code)
387+
303388

304389
class TestPyReplOutput(TestCase):
305390
def prepare_reader(self, events):
@@ -316,14 +401,12 @@ def test_basic(self):
316401

317402
def test_multiline_edit(self):
318403
events = itertools.chain(
319-
code_to_events("def f():\n ...\n\n"),
404+
code_to_events("def f():\n...\n\n"),
320405
[
321406
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
322407
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
323408
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
324409
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
325-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
326-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
327410
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
328411
Event(evt="key", data="g", raw=bytearray(b"g")),
329412
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
@@ -334,9 +417,9 @@ def test_multiline_edit(self):
334417
reader = self.prepare_reader(events)
335418

336419
output = multiline_input(reader)
337-
self.assertEqual(output, "def f():\n ...\n ")
420+
self.assertEqual(output, "def f():\n ...\n ")
338421
output = multiline_input(reader)
339-
self.assertEqual(output, "def g():\n ...\n ")
422+
self.assertEqual(output, "def g():\n ...\n ")
340423

341424
def test_history_navigation_with_up_arrow(self):
342425
events = itertools.chain(
@@ -485,6 +568,7 @@ class Dummy:
485568
@property
486569
def test_func(self):
487570
import warnings
571+
488572
warnings.warn("warnings\n")
489573
return None
490574

@@ -508,12 +592,12 @@ def prepare_reader(self, events):
508592
def test_paste(self):
509593
# fmt: off
510594
code = (
511-
'def a():\n'
512-
' for x in range(10):\n'
513-
' if x%2:\n'
514-
' print(x)\n'
515-
' else:\n'
516-
' pass\n'
595+
"def a():\n"
596+
" for x in range(10):\n"
597+
" if x%2:\n"
598+
" print(x)\n"
599+
" else:\n"
600+
" pass\n"
517601
)
518602
# fmt: on
519603

@@ -534,10 +618,10 @@ def test_paste(self):
534618
def test_paste_mid_newlines(self):
535619
# fmt: off
536620
code = (
537-
'def f():\n'
538-
' x = y\n'
539-
' \n'
540-
' y = z\n'
621+
"def f():\n"
622+
" x = y\n"
623+
" \n"
624+
" y = z\n"
541625
)
542626
# fmt: on
543627

@@ -558,16 +642,16 @@ def test_paste_mid_newlines(self):
558642
def test_paste_mid_newlines_not_in_paste_mode(self):
559643
# fmt: off
560644
code = (
561-
'def f():\n'
562-
' x = y\n'
563-
' \n'
564-
' y = z\n\n'
645+
"def f():\n"
646+
"x = y\n"
647+
"\n"
648+
"y = z\n\n"
565649
)
566650

567651
expected = (
568-
'def f():\n'
569-
' x = y\n'
570-
' '
652+
"def f():\n"
653+
" x = y\n"
654+
" "
571655
)
572656
# fmt: on
573657

@@ -579,20 +663,20 @@ def test_paste_mid_newlines_not_in_paste_mode(self):
579663
def test_paste_not_in_paste_mode(self):
580664
# fmt: off
581665
input_code = (
582-
'def a():\n'
583-
' for x in range(10):\n'
584-
' if x%2:\n'
585-
' print(x)\n'
586-
' else:\n'
587-
' pass\n\n'
666+
"def a():\n"
667+
"for x in range(10):\n"
668+
"if x%2:\n"
669+
"print(x)\n"
670+
"else:\n"
671+
"pass\n\n"
588672
)
589673

590674
output_code = (
591-
'def a():\n'
592-
' for x in range(10):\n'
593-
' if x%2:\n'
594-
' print(x)\n'
595-
' else:'
675+
"def a():\n"
676+
" for x in range(10):\n"
677+
" if x%2:\n"
678+
" print(x)\n"
679+
" else:"
596680
)
597681
# fmt: on
598682

@@ -605,25 +689,25 @@ def test_bracketed_paste(self):
605689
"""Test that bracketed paste using \x1b[200~ and \x1b[201~ works."""
606690
# fmt: off
607691
input_code = (
608-
'def a():\n'
609-
' for x in range(10):\n'
610-
'\n'
611-
' if x%2:\n'
612-
' print(x)\n'
613-
'\n'
614-
' else:\n'
615-
' pass\n'
692+
"def a():\n"
693+
" for x in range(10):\n"
694+
"\n"
695+
" if x%2:\n"
696+
" print(x)\n"
697+
"\n"
698+
" else:\n"
699+
" pass\n"
616700
)
617701

618702
output_code = (
619-
'def a():\n'
620-
' for x in range(10):\n'
621-
'\n'
622-
' if x%2:\n'
623-
' print(x)\n'
624-
'\n'
625-
' else:\n'
626-
' pass\n'
703+
"def a():\n"
704+
" for x in range(10):\n"
705+
"\n"
706+
" if x%2:\n"
707+
" print(x)\n"
708+
"\n"
709+
" else:\n"
710+
" pass\n"
627711
)
628712
# fmt: on
629713

0 commit comments

Comments
 (0)