• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import io
2import itertools
3import os
4import pathlib
5import re
6import rlcompleter
7import select
8import subprocess
9import sys
10import tempfile
11from unittest import TestCase, skipUnless, skipIf
12from unittest.mock import patch
13from test.support import force_not_colorized
14from test.support import SHORT_TIMEOUT
15from test.support.import_helper import import_module
16from test.support.os_helper import unlink
17
18from .support import (
19    FakeConsole,
20    handle_all_events,
21    handle_events_narrow_console,
22    more_lines,
23    multiline_input,
24    code_to_events,
25    clean_screen,
26    make_clean_env,
27)
28from _pyrepl.console import Event
29from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig,
30                              _ReadlineWrapper)
31from _pyrepl.readline import multiline_input as readline_multiline_input
32
33try:
34    import pty
35except ImportError:
36    pty = None
37
38
39class ReplTestCase(TestCase):
40    def run_repl(
41        self,
42        repl_input: str | list[str],
43        env: dict | None = None,
44        *,
45        cmdline_args: list[str] | None = None,
46        cwd: str | None = None,
47    ) -> tuple[str, int]:
48        temp_dir = None
49        if cwd is None:
50            temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
51            cwd = temp_dir.name
52        try:
53            return self._run_repl(
54                repl_input, env=env, cmdline_args=cmdline_args, cwd=cwd
55            )
56        finally:
57            if temp_dir is not None:
58                temp_dir.cleanup()
59
60    def _run_repl(
61        self,
62        repl_input: str | list[str],
63        *,
64        env: dict | None,
65        cmdline_args: list[str] | None,
66        cwd: str,
67    ) -> tuple[str, int]:
68        assert pty
69        master_fd, slave_fd = pty.openpty()
70        cmd = [sys.executable, "-i", "-u"]
71        if env is None:
72            cmd.append("-I")
73        elif "PYTHON_HISTORY" not in env:
74            env["PYTHON_HISTORY"] = os.path.join(cwd, ".regrtest_history")
75        if cmdline_args is not None:
76            cmd.extend(cmdline_args)
77
78        try:
79            import termios
80        except ModuleNotFoundError:
81            pass
82        else:
83            term_attr = termios.tcgetattr(slave_fd)
84            term_attr[6][termios.VREPRINT] = 0  # pass through CTRL-R
85            term_attr[6][termios.VINTR] = 0  # pass through CTRL-C
86            termios.tcsetattr(slave_fd, termios.TCSANOW, term_attr)
87
88        process = subprocess.Popen(
89            cmd,
90            stdin=slave_fd,
91            stdout=slave_fd,
92            stderr=slave_fd,
93            cwd=cwd,
94            text=True,
95            close_fds=True,
96            env=env if env else os.environ,
97        )
98        os.close(slave_fd)
99        if isinstance(repl_input, list):
100            repl_input = "\n".join(repl_input) + "\n"
101        os.write(master_fd, repl_input.encode("utf-8"))
102
103        output = []
104        while select.select([master_fd], [], [], SHORT_TIMEOUT)[0]:
105            try:
106                data = os.read(master_fd, 1024).decode("utf-8")
107                if not data:
108                    break
109            except OSError:
110                break
111            output.append(data)
112        else:
113            os.close(master_fd)
114            process.kill()
115            self.fail(f"Timeout while waiting for output, got: {''.join(output)}")
116
117        os.close(master_fd)
118        try:
119            exit_code = process.wait(timeout=SHORT_TIMEOUT)
120        except subprocess.TimeoutExpired:
121            process.kill()
122            exit_code = process.wait()
123        return "".join(output), exit_code
124
125
126class TestCursorPosition(TestCase):
127    def prepare_reader(self, events):
128        console = FakeConsole(events)
129        config = ReadlineConfig(readline_completer=None)
130        reader = ReadlineAlikeReader(console=console, config=config)
131        return reader
132
133    def test_up_arrow_simple(self):
134        # fmt: off
135        code = (
136            "def f():\n"
137            "  ...\n"
138        )
139        # fmt: on
140        events = itertools.chain(
141            code_to_events(code),
142            [
143                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
144            ],
145        )
146
147        reader, console = handle_all_events(events)
148        self.assertEqual(reader.cxy, (0, 1))
149        console.move_cursor.assert_called_once_with(0, 1)
150
151    def test_down_arrow_end_of_input(self):
152        # fmt: off
153        code = (
154            "def f():\n"
155            "  ...\n"
156        )
157        # fmt: on
158        events = itertools.chain(
159            code_to_events(code),
160            [
161                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
162            ],
163        )
164
165        reader, console = handle_all_events(events)
166        self.assertEqual(reader.cxy, (0, 2))
167        console.move_cursor.assert_called_once_with(0, 2)
168
169    def test_left_arrow_simple(self):
170        events = itertools.chain(
171            code_to_events("11+11"),
172            [
173                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
174            ],
175        )
176
177        reader, console = handle_all_events(events)
178        self.assertEqual(reader.cxy, (4, 0))
179        console.move_cursor.assert_called_once_with(4, 0)
180
181    def test_right_arrow_end_of_line(self):
182        events = itertools.chain(
183            code_to_events("11+11"),
184            [
185                Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
186            ],
187        )
188
189        reader, console = handle_all_events(events)
190        self.assertEqual(reader.cxy, (5, 0))
191        console.move_cursor.assert_called_once_with(5, 0)
192
193    def test_cursor_position_simple_character(self):
194        events = itertools.chain(code_to_events("k"))
195
196        reader, _ = handle_all_events(events)
197        self.assertEqual(reader.pos, 1)
198
199        # 1 for simple character
200        self.assertEqual(reader.cxy, (1, 0))
201
202    def test_cursor_position_double_width_character(self):
203        events = itertools.chain(code_to_events("樂"))
204
205        reader, _ = handle_all_events(events)
206        self.assertEqual(reader.pos, 1)
207
208        # 2 for wide character
209        self.assertEqual(reader.cxy, (2, 0))
210
211    def test_cursor_position_double_width_character_move_left(self):
212        events = itertools.chain(
213            code_to_events("樂"),
214            [
215                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
216            ],
217        )
218
219        reader, _ = handle_all_events(events)
220        self.assertEqual(reader.pos, 0)
221        self.assertEqual(reader.cxy, (0, 0))
222
223    def test_cursor_position_double_width_character_move_left_right(self):
224        events = itertools.chain(
225            code_to_events("樂"),
226            [
227                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
228                Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
229            ],
230        )
231
232        reader, _ = handle_all_events(events)
233        self.assertEqual(reader.pos, 1)
234
235        # 2 for wide character
236        self.assertEqual(reader.cxy, (2, 0))
237
238    def test_cursor_position_double_width_characters_move_up(self):
239        for_loop = "for _ in _:"
240
241        # fmt: off
242        code = (
243           f"{for_loop}\n"
244            "  ' 可口可乐; 可口可樂'"
245        )
246        # fmt: on
247
248        events = itertools.chain(
249            code_to_events(code),
250            [
251                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
252            ],
253        )
254
255        reader, _ = handle_all_events(events)
256
257        # cursor at end of first line
258        self.assertEqual(reader.pos, len(for_loop))
259        self.assertEqual(reader.cxy, (len(for_loop), 0))
260
261    def test_cursor_position_double_width_characters_move_up_down(self):
262        for_loop = "for _ in _:"
263
264        # fmt: off
265        code = (
266           f"{for_loop}\n"
267            "  ' 可口可乐; 可口可樂'"
268        )
269        # fmt: on
270
271        events = itertools.chain(
272            code_to_events(code),
273            [
274                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
275                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
276                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
277            ],
278        )
279
280        reader, _ = handle_all_events(events)
281
282        # cursor here (showing 2nd line only):
283        # <  ' 可口可乐; 可口可樂'>
284        #              ^
285        self.assertEqual(reader.pos, 19)
286        self.assertEqual(reader.cxy, (10, 1))
287
288    def test_cursor_position_multiple_double_width_characters_move_left(self):
289        events = itertools.chain(
290            code_to_events("' 可口可乐; 可口可樂'"),
291            [
292                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
293                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
294                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
295            ],
296        )
297
298        reader, _ = handle_all_events(events)
299        self.assertEqual(reader.pos, 10)
300
301        # 1 for quote, 1 for space, 2 per wide character,
302        # 1 for semicolon, 1 for space, 2 per wide character
303        self.assertEqual(reader.cxy, (16, 0))
304
305    def test_cursor_position_move_up_to_eol(self):
306        first_line = "for _ in _:"
307        second_line = "  hello"
308
309        # fmt: off
310        code = (
311            f"{first_line}\n"
312            f"{second_line}\n"
313             "  h\n"
314             "  hel"
315        )
316        # fmt: on
317
318        events = itertools.chain(
319            code_to_events(code),
320            [
321                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
322                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
323            ],
324        )
325
326        reader, _ = handle_all_events(events)
327
328        # Cursor should be at end of line 1, even though line 2 is shorter
329        # for _ in _:
330        #   hello
331        #   h
332        #   hel
333        self.assertEqual(
334            reader.pos, len(first_line) + len(second_line) + 1
335        )  # +1 for newline
336        self.assertEqual(reader.cxy, (len(second_line), 1))
337
338    def test_cursor_position_move_down_to_eol(self):
339        last_line = "  hel"
340
341        # fmt: off
342        code = (
343            "for _ in _:\n"
344            "  hello\n"
345            "  h\n"
346           f"{last_line}"
347        )
348        # fmt: on
349
350        events = itertools.chain(
351            code_to_events(code),
352            [
353                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
354                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
355                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
356                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
357            ],
358        )
359
360        reader, _ = handle_all_events(events)
361
362        # Cursor should be at end of line 3, even though line 2 is shorter
363        # for _ in _:
364        #   hello
365        #   h
366        #   hel
367        self.assertEqual(reader.pos, len(code))
368        self.assertEqual(reader.cxy, (len(last_line), 3))
369
370    def test_cursor_position_multiple_mixed_lines_move_up(self):
371        # fmt: off
372        code = (
373            "def foo():\n"
374            "  x = '可口可乐; 可口可樂'\n"
375            "  y = 'abckdfjskldfjslkdjf'"
376        )
377        # fmt: on
378
379        events = itertools.chain(
380            code_to_events(code),
381            13 * [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
382            [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
383        )
384
385        reader, _ = handle_all_events(events)
386
387        # By moving left, we're before the s:
388        # y = 'abckdfjskldfjslkdjf'
389        #             ^
390        # And we should move before the semi-colon despite the different offset
391        # x = '可口可乐; 可口可樂'
392        #            ^
393        self.assertEqual(reader.pos, 22)
394        self.assertEqual(reader.cxy, (15, 1))
395
396    def test_cursor_position_after_wrap_and_move_up(self):
397        # fmt: off
398        code = (
399            "def foo():\n"
400            "  hello"
401        )
402        # fmt: on
403
404        events = itertools.chain(
405            code_to_events(code),
406            [
407                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
408            ],
409        )
410        reader, _ = handle_events_narrow_console(events)
411
412        # The code looks like this:
413        # def foo()\
414        # :
415        #   hello
416        # After moving up we should be after the colon in line 2
417        self.assertEqual(reader.pos, 10)
418        self.assertEqual(reader.cxy, (1, 1))
419
420
421class TestPyReplAutoindent(TestCase):
422    def prepare_reader(self, events):
423        console = FakeConsole(events)
424        config = ReadlineConfig(readline_completer=None)
425        reader = ReadlineAlikeReader(console=console, config=config)
426        return reader
427
428    def test_auto_indent_default(self):
429        # fmt: off
430        input_code = (
431            "def f():\n"
432                "pass\n\n"
433        )
434
435        output_code = (
436            "def f():\n"
437            "    pass\n"
438            "    "
439        )
440        # fmt: on
441
442    def test_auto_indent_continuation(self):
443        # auto indenting according to previous user indentation
444        # fmt: off
445        events = itertools.chain(
446            code_to_events("def f():\n"),
447            # add backspace to delete default auto-indent
448            [
449                Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
450            ],
451            code_to_events(
452                "  pass\n"
453                  "pass\n\n"
454            ),
455        )
456
457        output_code = (
458            "def f():\n"
459            "  pass\n"
460            "  pass\n"
461            "  "
462        )
463        # fmt: on
464
465        reader = self.prepare_reader(events)
466        output = multiline_input(reader)
467        self.assertEqual(output, output_code)
468
469    def test_auto_indent_prev_block(self):
470        # auto indenting according to indentation in different block
471        # fmt: off
472        events = itertools.chain(
473            code_to_events("def f():\n"),
474            # add backspace to delete default auto-indent
475            [
476                Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
477            ],
478            code_to_events(
479                "  pass\n"
480                "pass\n\n"
481            ),
482            code_to_events(
483                "def g():\n"
484                  "pass\n\n"
485            ),
486        )
487
488        output_code = (
489            "def g():\n"
490            "  pass\n"
491            "  "
492        )
493        # fmt: on
494
495        reader = self.prepare_reader(events)
496        output1 = multiline_input(reader)
497        output2 = multiline_input(reader)
498        self.assertEqual(output2, output_code)
499
500    def test_auto_indent_multiline(self):
501        # fmt: off
502        events = itertools.chain(
503            code_to_events(
504                "def f():\n"
505                    "pass"
506            ),
507            [
508                # go to the end of the first line
509                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
510                Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
511                # new line should be autoindented
512                Event(evt="key", data="\n", raw=bytearray(b"\n")),
513            ],
514            code_to_events(
515                "pass"
516            ),
517            [
518                # go to end of last line
519                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
520                Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
521                # double newline to terminate the block
522                Event(evt="key", data="\n", raw=bytearray(b"\n")),
523                Event(evt="key", data="\n", raw=bytearray(b"\n")),
524            ],
525        )
526
527        output_code = (
528            "def f():\n"
529            "    pass\n"
530            "    pass\n"
531            "    "
532        )
533        # fmt: on
534
535        reader = self.prepare_reader(events)
536        output = multiline_input(reader)
537        self.assertEqual(output, output_code)
538
539    def test_auto_indent_with_comment(self):
540        # fmt: off
541        events = code_to_events(
542            "def f():  # foo\n"
543                "pass\n\n"
544        )
545
546        output_code = (
547            "def f():  # foo\n"
548            "    pass\n"
549            "    "
550        )
551        # fmt: on
552
553        reader = self.prepare_reader(events)
554        output = multiline_input(reader)
555        self.assertEqual(output, output_code)
556
557    def test_auto_indent_with_multicomment(self):
558        # fmt: off
559        events = code_to_events(
560            "def f():  ## foo\n"
561                "pass\n\n"
562        )
563
564        output_code = (
565            "def f():  ## foo\n"
566            "    pass\n"
567            "    "
568        )
569        # fmt: on
570
571        reader = self.prepare_reader(events)
572        output = multiline_input(reader)
573        self.assertEqual(output, output_code)
574
575    def test_auto_indent_ignore_comments(self):
576        # fmt: off
577        events = code_to_events(
578            "pass  #:\n"
579        )
580
581        output_code = (
582            "pass  #:"
583        )
584        # fmt: on
585
586        reader = self.prepare_reader(events)
587        output = multiline_input(reader)
588        self.assertEqual(output, output_code)
589
590
591class TestPyReplOutput(TestCase):
592    def prepare_reader(self, events):
593        console = FakeConsole(events)
594        config = ReadlineConfig(readline_completer=None)
595        reader = ReadlineAlikeReader(console=console, config=config)
596        reader.can_colorize = False
597        return reader
598
599    def test_stdin_is_tty(self):
600        # Used during test log analysis to figure out if a TTY was available.
601        try:
602            if os.isatty(sys.stdin.fileno()):
603                return
604        except OSError as ose:
605            self.skipTest(f"stdin tty check failed: {ose}")
606        else:
607            self.skipTest("stdin is not a tty")
608
609    def test_stdout_is_tty(self):
610        # Used during test log analysis to figure out if a TTY was available.
611        try:
612            if os.isatty(sys.stdout.fileno()):
613                return
614        except OSError as ose:
615            self.skipTest(f"stdout tty check failed: {ose}")
616        else:
617            self.skipTest("stdout is not a tty")
618
619    def test_basic(self):
620        reader = self.prepare_reader(code_to_events("1+1\n"))
621
622        output = multiline_input(reader)
623        self.assertEqual(output, "1+1")
624        self.assertEqual(clean_screen(reader.screen), "1+1")
625
626    def test_get_line_buffer_returns_str(self):
627        reader = self.prepare_reader(code_to_events("\n"))
628        wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader)
629        self.assertIs(type(wrapper.get_line_buffer()), str)
630
631    def test_multiline_edit(self):
632        events = itertools.chain(
633            code_to_events("def f():\n...\n\n"),
634            [
635                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
636                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
637                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
638                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
639                Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
640                Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
641                Event(evt="key", data="g", raw=bytearray(b"g")),
642                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
643                Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
644                Event(evt="key", data="delete", raw=bytearray(b"\x7F")),
645                Event(evt="key", data="right", raw=bytearray(b"g")),
646                Event(evt="key", data="backspace", raw=bytearray(b"\x08")),
647                Event(evt="key", data="p", raw=bytearray(b"p")),
648                Event(evt="key", data="a", raw=bytearray(b"a")),
649                Event(evt="key", data="s", raw=bytearray(b"s")),
650                Event(evt="key", data="s", raw=bytearray(b"s")),
651                Event(evt="key", data="\n", raw=bytearray(b"\n")),
652                Event(evt="key", data="\n", raw=bytearray(b"\n")),
653            ],
654        )
655        reader = self.prepare_reader(events)
656
657        output = multiline_input(reader)
658        self.assertEqual(output, "def f():\n    ...\n    ")
659        self.assertEqual(clean_screen(reader.screen), "def f():\n    ...")
660        output = multiline_input(reader)
661        self.assertEqual(output, "def g():\n    pass\n    ")
662        self.assertEqual(clean_screen(reader.screen), "def g():\n    pass")
663
664    def test_history_navigation_with_up_arrow(self):
665        events = itertools.chain(
666            code_to_events("1+1\n2+2\n"),
667            [
668                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
669                Event(evt="key", data="\n", raw=bytearray(b"\n")),
670                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
671                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
672                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
673                Event(evt="key", data="\n", raw=bytearray(b"\n")),
674            ],
675        )
676
677        reader = self.prepare_reader(events)
678
679        output = multiline_input(reader)
680        self.assertEqual(output, "1+1")
681        self.assertEqual(clean_screen(reader.screen), "1+1")
682        output = multiline_input(reader)
683        self.assertEqual(output, "2+2")
684        self.assertEqual(clean_screen(reader.screen), "2+2")
685        output = multiline_input(reader)
686        self.assertEqual(output, "2+2")
687        self.assertEqual(clean_screen(reader.screen), "2+2")
688        output = multiline_input(reader)
689        self.assertEqual(output, "1+1")
690        self.assertEqual(clean_screen(reader.screen), "1+1")
691
692    def test_history_with_multiline_entries(self):
693        code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
694        events = list(itertools.chain(
695            code_to_events(code),
696            [
697                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
698                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
699                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
700                Event(evt="key", data="\n", raw=bytearray(b"\n")),
701                Event(evt="key", data="\n", raw=bytearray(b"\n")),
702            ]
703        ))
704
705        reader = self.prepare_reader(events)
706        output = multiline_input(reader)
707        output = multiline_input(reader)
708        output = multiline_input(reader)
709        self.assertEqual(
710            clean_screen(reader.screen),
711            'def foo():\n    x = 1\n    y = 2\n    z = 3'
712        )
713        self.assertEqual(output, "def foo():\n    x = 1\n    y = 2\n    z = 3\n    ")
714
715
716    def test_history_navigation_with_down_arrow(self):
717        events = itertools.chain(
718            code_to_events("1+1\n2+2\n"),
719            [
720                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
721                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
722                Event(evt="key", data="\n", raw=bytearray(b"\n")),
723                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
724                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
725            ],
726        )
727
728        reader = self.prepare_reader(events)
729
730        output = multiline_input(reader)
731        self.assertEqual(output, "1+1")
732        self.assertEqual(clean_screen(reader.screen), "1+1")
733
734    def test_history_search(self):
735        events = itertools.chain(
736            code_to_events("1+1\n2+2\n3+3\n"),
737            [
738                Event(evt="key", data="\x12", raw=bytearray(b"\x12")),
739                Event(evt="key", data="1", raw=bytearray(b"1")),
740                Event(evt="key", data="\n", raw=bytearray(b"\n")),
741                Event(evt="key", data="\n", raw=bytearray(b"\n")),
742            ],
743        )
744
745        reader = self.prepare_reader(events)
746
747        output = multiline_input(reader)
748        self.assertEqual(output, "1+1")
749        self.assertEqual(clean_screen(reader.screen), "1+1")
750        output = multiline_input(reader)
751        self.assertEqual(output, "2+2")
752        self.assertEqual(clean_screen(reader.screen), "2+2")
753        output = multiline_input(reader)
754        self.assertEqual(output, "3+3")
755        self.assertEqual(clean_screen(reader.screen), "3+3")
756        output = multiline_input(reader)
757        self.assertEqual(output, "1+1")
758        self.assertEqual(clean_screen(reader.screen), "1+1")
759
760    def test_control_character(self):
761        events = code_to_events("c\x1d\n")
762        reader = self.prepare_reader(events)
763        output = multiline_input(reader)
764        self.assertEqual(output, "c\x1d")
765        self.assertEqual(clean_screen(reader.screen), "c")
766
767    def test_history_search_backward(self):
768        # Test <page up> history search backward with "imp" input
769        events = itertools.chain(
770            code_to_events("import os\n"),
771            code_to_events("imp"),
772            [
773                Event(evt='key', data='page up', raw=bytearray(b'\x1b[5~')),
774                Event(evt="key", data="\n", raw=bytearray(b"\n")),
775            ],
776        )
777
778        # fill the history
779        reader = self.prepare_reader(events)
780        multiline_input(reader)
781
782        # search for "imp" in history
783        output = multiline_input(reader)
784        self.assertEqual(output, "import os")
785        self.assertEqual(clean_screen(reader.screen), "import os")
786
787    def test_history_search_backward_empty(self):
788        # Test <page up> history search backward with an empty input
789        events = itertools.chain(
790            code_to_events("import os\n"),
791            [
792                Event(evt='key', data='page up', raw=bytearray(b'\x1b[5~')),
793                Event(evt="key", data="\n", raw=bytearray(b"\n")),
794            ],
795        )
796
797        # fill the history
798        reader = self.prepare_reader(events)
799        multiline_input(reader)
800
801        # search backward in history
802        output = multiline_input(reader)
803        self.assertEqual(output, "import os")
804        self.assertEqual(clean_screen(reader.screen), "import os")
805
806
807class TestPyReplCompleter(TestCase):
808    def prepare_reader(self, events, namespace):
809        console = FakeConsole(events)
810        config = ReadlineConfig()
811        config.readline_completer = rlcompleter.Completer(namespace).complete
812        reader = ReadlineAlikeReader(console=console, config=config)
813        return reader
814
815    @patch("rlcompleter._readline_available", False)
816    def test_simple_completion(self):
817        events = code_to_events("os.getpid\t\n")
818
819        namespace = {"os": os}
820        reader = self.prepare_reader(events, namespace)
821
822        output = multiline_input(reader, namespace)
823        self.assertEqual(output, "os.getpid()")
824
825    def test_completion_with_many_options(self):
826        # Test with something that initially displays many options
827        # and then complete from one of them. The first time tab is
828        # pressed, the options are displayed (which corresponds to
829        # when the repl shows [ not unique ]) and the second completes
830        # from one of them.
831        events = code_to_events("os.\t\tO_AP\t\n")
832
833        namespace = {"os": os}
834        reader = self.prepare_reader(events, namespace)
835
836        output = multiline_input(reader, namespace)
837        self.assertEqual(output, "os.O_APPEND")
838
839    def test_empty_namespace_completion(self):
840        events = code_to_events("os.geten\t\n")
841        namespace = {}
842        reader = self.prepare_reader(events, namespace)
843
844        output = multiline_input(reader, namespace)
845        self.assertEqual(output, "os.geten")
846
847    def test_global_namespace_completion(self):
848        events = code_to_events("py\t\n")
849        namespace = {"python": None}
850        reader = self.prepare_reader(events, namespace)
851        output = multiline_input(reader, namespace)
852        self.assertEqual(output, "python")
853
854    def test_updown_arrow_with_completion_menu(self):
855        """Up arrow in the middle of unfinished tab completion when the menu is displayed
856        should work and trigger going back in history. Down arrow should subsequently
857        get us back to the incomplete command."""
858        code = "import os\nos.\t\t"
859        namespace = {"os": os}
860
861        events = itertools.chain(
862            code_to_events(code),
863            [
864                Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
865                Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
866            ],
867            code_to_events("\n"),
868        )
869        reader = self.prepare_reader(events, namespace=namespace)
870        output = multiline_input(reader, namespace)
871        # This is the first line, nothing to see here
872        self.assertEqual(output, "import os")
873        # This is the second line. We pressed up and down arrows
874        # so we should end up where we were when we initiated tab completion.
875        output = multiline_input(reader, namespace)
876        self.assertEqual(output, "os.")
877
878    @patch("_pyrepl.readline._ReadlineWrapper.get_reader")
879    @patch("sys.stderr", new_callable=io.StringIO)
880    def test_completion_with_warnings(self, mock_stderr, mock_get_reader):
881        class Dummy:
882            @property
883            def test_func(self):
884                import warnings
885
886                warnings.warn("warnings\n")
887                return None
888
889        dummy = Dummy()
890        events = code_to_events("dummy.test_func.\t\n\n")
891        namespace = {"dummy": dummy}
892        reader = self.prepare_reader(events, namespace)
893        mock_get_reader.return_value = reader
894        output = readline_multiline_input(more_lines, ">>>", "...")
895        self.assertEqual(output, "dummy.test_func.__")
896        self.assertEqual(mock_stderr.getvalue(), "")
897
898
899class TestPasteEvent(TestCase):
900    def prepare_reader(self, events):
901        console = FakeConsole(events)
902        config = ReadlineConfig(readline_completer=None)
903        reader = ReadlineAlikeReader(console=console, config=config)
904        return reader
905
906    def test_paste(self):
907        # fmt: off
908        code = (
909            "def a():\n"
910            "  for x in range(10):\n"
911            "    if x%2:\n"
912            "      print(x)\n"
913            "    else:\n"
914            "      pass\n"
915        )
916        # fmt: on
917
918        events = itertools.chain(
919            [
920                Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
921            ],
922            code_to_events(code),
923            [
924                Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
925            ],
926            code_to_events("\n"),
927        )
928        reader = self.prepare_reader(events)
929        output = multiline_input(reader)
930        self.assertEqual(output, code)
931
932    def test_paste_mid_newlines(self):
933        # fmt: off
934        code = (
935            "def f():\n"
936            "  x = y\n"
937            "  \n"
938            "  y = z\n"
939        )
940        # fmt: on
941
942        events = itertools.chain(
943            [
944                Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
945            ],
946            code_to_events(code),
947            [
948                Event(evt="key", data="f3", raw=bytearray(b"\x1bOR")),
949            ],
950            code_to_events("\n"),
951        )
952        reader = self.prepare_reader(events)
953        output = multiline_input(reader)
954        self.assertEqual(output, code)
955
956    def test_paste_mid_newlines_not_in_paste_mode(self):
957        # fmt: off
958        code = (
959            "def f():\n"
960                "x = y\n"
961                "\n"
962                "y = z\n\n"
963        )
964
965        expected = (
966            "def f():\n"
967            "    x = y\n"
968            "    "
969        )
970        # fmt: on
971
972        events = code_to_events(code)
973        reader = self.prepare_reader(events)
974        output = multiline_input(reader)
975        self.assertEqual(output, expected)
976
977    def test_paste_not_in_paste_mode(self):
978        # fmt: off
979        input_code = (
980            "def a():\n"
981                "for x in range(10):\n"
982                    "if x%2:\n"
983                        "print(x)\n"
984                    "else:\n"
985                        "pass\n\n"
986        )
987
988        output_code = (
989            "def a():\n"
990            "    for x in range(10):\n"
991            "        if x%2:\n"
992            "            print(x)\n"
993            "            else:"
994        )
995        # fmt: on
996
997        events = code_to_events(input_code)
998        reader = self.prepare_reader(events)
999        output = multiline_input(reader)
1000        self.assertEqual(output, output_code)
1001
1002    def test_bracketed_paste(self):
1003        """Test that bracketed paste using \x1b[200~ and \x1b[201~ works."""
1004        # fmt: off
1005        input_code = (
1006            "def a():\n"
1007            "  for x in range(10):\n"
1008            "\n"
1009            "    if x%2:\n"
1010            "      print(x)\n"
1011            "\n"
1012            "    else:\n"
1013            "      pass\n"
1014        )
1015
1016        output_code = (
1017            "def a():\n"
1018            "  for x in range(10):\n"
1019            "\n"
1020            "    if x%2:\n"
1021            "      print(x)\n"
1022            "\n"
1023            "    else:\n"
1024            "      pass\n"
1025        )
1026        # fmt: on
1027
1028        paste_start = "\x1b[200~"
1029        paste_end = "\x1b[201~"
1030
1031        events = itertools.chain(
1032            code_to_events(paste_start),
1033            code_to_events(input_code),
1034            code_to_events(paste_end),
1035            code_to_events("\n"),
1036        )
1037        reader = self.prepare_reader(events)
1038        output = multiline_input(reader)
1039        self.assertEqual(output, output_code)
1040
1041    def test_bracketed_paste_single_line(self):
1042        input_code = "oneline"
1043
1044        paste_start = "\x1b[200~"
1045        paste_end = "\x1b[201~"
1046
1047        events = itertools.chain(
1048            code_to_events(paste_start),
1049            code_to_events(input_code),
1050            code_to_events(paste_end),
1051            code_to_events("\n"),
1052        )
1053        reader = self.prepare_reader(events)
1054        output = multiline_input(reader)
1055        self.assertEqual(output, input_code)
1056
1057
1058@skipUnless(pty, "requires pty")
1059class TestDumbTerminal(ReplTestCase):
1060    def test_dumb_terminal_exits_cleanly(self):
1061        env = os.environ.copy()
1062        env.update({"TERM": "dumb"})
1063        output, exit_code = self.run_repl("exit()\n", env=env)
1064        self.assertEqual(exit_code, 0)
1065        self.assertIn("warning: can't use pyrepl", output)
1066        self.assertNotIn("Exception", output)
1067        self.assertNotIn("Traceback", output)
1068
1069
1070@skipUnless(pty, "requires pty")
1071@skipIf((os.environ.get("TERM") or "dumb") == "dumb", "can't use pyrepl in dumb terminal")
1072class TestMain(ReplTestCase):
1073    def setUp(self):
1074        # Cleanup from PYTHON* variables to isolate from local
1075        # user settings, see #121359.  Such variables should be
1076        # added later in test methods to patched os.environ.
1077        patcher = patch('os.environ', new=make_clean_env())
1078        self.addCleanup(patcher.stop)
1079        patcher.start()
1080
1081    @force_not_colorized
1082    def test_exposed_globals_in_repl(self):
1083        pre = "['__annotations__', '__builtins__'"
1084        post = "'__loader__', '__name__', '__package__', '__spec__']"
1085        output, exit_code = self.run_repl(["sorted(dir())", "exit()"])
1086        if "can't use pyrepl" in output:
1087            self.skipTest("pyrepl not available")
1088        self.assertEqual(exit_code, 0)
1089
1090        # if `__main__` is not a file (impossible with pyrepl)
1091        case1 = f"{pre}, '__doc__', {post}" in output
1092
1093        # if `__main__` is an uncached .py file (no .pyc)
1094        case2 = f"{pre}, '__doc__', '__file__', {post}" in output
1095
1096        # if `__main__` is a cached .pyc file and the .py source exists
1097        case3 = f"{pre}, '__cached__', '__doc__', '__file__', {post}" in output
1098
1099        # if `__main__` is a cached .pyc file but there's no .py source file
1100        case4 = f"{pre}, '__cached__', '__doc__', {post}" in output
1101
1102        self.assertTrue(case1 or case2 or case3 or case4, output)
1103
1104    def _assertMatchOK(
1105            self, var: str, expected: str | re.Pattern, actual: str
1106    ) -> None:
1107        if isinstance(expected, re.Pattern):
1108            self.assertTrue(
1109                expected.match(actual),
1110                f"{var}={actual} does not match {expected.pattern}",
1111            )
1112        else:
1113            self.assertEqual(
1114                actual,
1115                expected,
1116                f"expected {var}={expected}, got {var}={actual}",
1117            )
1118
1119    @force_not_colorized
1120    def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False):
1121        clean_env = make_clean_env()
1122        clean_env["NO_COLOR"] = "1"  # force_not_colorized doesn't touch subprocesses
1123
1124        with tempfile.TemporaryDirectory() as td:
1125            blue = pathlib.Path(td) / "blue"
1126            blue.mkdir()
1127            mod = blue / "calx.py"
1128            mod.write_text("FOO = 42", encoding="utf-8")
1129            commands = [
1130                "print(f'^{" + var + "=}')" for var in expectations
1131            ] + ["exit()"]
1132            if as_file and as_module:
1133                self.fail("as_file and as_module are mutually exclusive")
1134            elif as_file:
1135                output, exit_code = self.run_repl(
1136                    commands,
1137                    cmdline_args=[str(mod)],
1138                    env=clean_env,
1139                )
1140            elif as_module:
1141                output, exit_code = self.run_repl(
1142                    commands,
1143                    cmdline_args=["-m", "blue.calx"],
1144                    env=clean_env,
1145                    cwd=td,
1146                )
1147            else:
1148                self.fail("Choose one of as_file or as_module")
1149
1150        if "can't use pyrepl" in output:
1151            self.skipTest("pyrepl not available")
1152
1153        self.assertEqual(exit_code, 0)
1154        for var, expected in expectations.items():
1155            with self.subTest(var=var, expected=expected):
1156                if m := re.search(rf"\^{var}=(.+?)[\r\n]", output):
1157                    self._assertMatchOK(var, expected, actual=m.group(1))
1158                else:
1159                    self.fail(f"{var}= not found in output: {output!r}\n\n{output}")
1160
1161        self.assertNotIn("Exception", output)
1162        self.assertNotIn("Traceback", output)
1163
1164    def test_inspect_keeps_globals_from_inspected_file(self):
1165        expectations = {
1166            "FOO": "42",
1167            "__name__": "'__main__'",
1168            "__package__": "None",
1169            # "__file__" is missing in -i, like in the basic REPL
1170        }
1171        self._run_repl_globals_test(expectations, as_file=True)
1172
1173    def test_inspect_keeps_globals_from_inspected_module(self):
1174        expectations = {
1175            "FOO": "42",
1176            "__name__": "'__main__'",
1177            "__package__": "'blue'",
1178            "__file__": re.compile(r"^'.*calx.py'$"),
1179        }
1180        self._run_repl_globals_test(expectations, as_module=True)
1181
1182    @force_not_colorized
1183    def test_python_basic_repl(self):
1184        env = os.environ.copy()
1185        commands = ("from test.support import initialized_with_pyrepl\n"
1186                    "initialized_with_pyrepl()\n"
1187                    "exit()\n")
1188
1189        env.pop("PYTHON_BASIC_REPL", None)
1190        output, exit_code = self.run_repl(commands, env=env)
1191        if "can\'t use pyrepl" in output:
1192            self.skipTest("pyrepl not available")
1193        self.assertEqual(exit_code, 0)
1194        self.assertIn("True", output)
1195        self.assertNotIn("False", output)
1196        self.assertNotIn("Exception", output)
1197        self.assertNotIn("Traceback", output)
1198
1199        env["PYTHON_BASIC_REPL"] = "1"
1200        output, exit_code = self.run_repl(commands, env=env)
1201        self.assertEqual(exit_code, 0)
1202        self.assertIn("False", output)
1203        self.assertNotIn("True", output)
1204        self.assertNotIn("Exception", output)
1205        self.assertNotIn("Traceback", output)
1206
1207        # The site module must not load _pyrepl if PYTHON_BASIC_REPL is set
1208        commands = ("import sys\n"
1209                    "print('_pyrepl' in sys.modules)\n"
1210                    "exit()\n")
1211        env["PYTHON_BASIC_REPL"] = "1"
1212        output, exit_code = self.run_repl(commands, env=env)
1213        self.assertEqual(exit_code, 0)
1214        self.assertIn("False", output)
1215        self.assertNotIn("True", output)
1216        self.assertNotIn("Exception", output)
1217        self.assertNotIn("Traceback", output)
1218
1219    @force_not_colorized
1220    def test_bad_sys_excepthook_doesnt_crash_pyrepl(self):
1221        env = os.environ.copy()
1222        commands = ("import sys\n"
1223                    "sys.excepthook = 1\n"
1224                    "1/0\n"
1225                    "exit()\n")
1226
1227        def check(output, exitcode):
1228            self.assertIn("Error in sys.excepthook:", output)
1229            self.assertEqual(output.count("'int' object is not callable"), 1)
1230            self.assertIn("Original exception was:", output)
1231            self.assertIn("division by zero", output)
1232            self.assertEqual(exitcode, 0)
1233        env.pop("PYTHON_BASIC_REPL", None)
1234        output, exit_code = self.run_repl(commands, env=env)
1235        if "can\'t use pyrepl" in output:
1236            self.skipTest("pyrepl not available")
1237        check(output, exit_code)
1238
1239        env["PYTHON_BASIC_REPL"] = "1"
1240        output, exit_code = self.run_repl(commands, env=env)
1241        check(output, exit_code)
1242
1243    def test_not_wiping_history_file(self):
1244        # skip, if readline module is not available
1245        import_module('readline')
1246
1247        hfile = tempfile.NamedTemporaryFile(delete=False)
1248        self.addCleanup(unlink, hfile.name)
1249        env = os.environ.copy()
1250        env["PYTHON_HISTORY"] = hfile.name
1251        commands = "123\nspam\nexit()\n"
1252
1253        env.pop("PYTHON_BASIC_REPL", None)
1254        output, exit_code = self.run_repl(commands, env=env)
1255        self.assertEqual(exit_code, 0)
1256        self.assertIn("123", output)
1257        self.assertIn("spam", output)
1258        self.assertNotEqual(pathlib.Path(hfile.name).stat().st_size, 0)
1259
1260        hfile.file.truncate()
1261        hfile.close()
1262
1263        env["PYTHON_BASIC_REPL"] = "1"
1264        output, exit_code = self.run_repl(commands, env=env)
1265        self.assertEqual(exit_code, 0)
1266        self.assertIn("123", output)
1267        self.assertIn("spam", output)
1268        self.assertNotEqual(pathlib.Path(hfile.name).stat().st_size, 0)
1269
1270    @force_not_colorized
1271    def test_correct_filename_in_syntaxerrors(self):
1272        env = os.environ.copy()
1273        commands = "a b c\nexit()\n"
1274        output, exit_code = self.run_repl(commands, env=env)
1275        if "can't use pyrepl" in output:
1276            self.skipTest("pyrepl not available")
1277        self.assertIn("SyntaxError: invalid syntax", output)
1278        self.assertIn("<python-input-0>", output)
1279        commands = " b\nexit()\n"
1280        output, exit_code = self.run_repl(commands, env=env)
1281        self.assertIn("IndentationError: unexpected indent", output)
1282        self.assertIn("<python-input-0>", output)
1283
1284    @force_not_colorized
1285    def test_proper_tracebacklimit(self):
1286        env = os.environ.copy()
1287        for set_tracebacklimit in [True, False]:
1288            commands = ("import sys\n" +
1289                        ("sys.tracebacklimit = 1\n" if set_tracebacklimit else "") +
1290                        "def x1(): 1/0\n\n"
1291                        "def x2(): x1()\n\n"
1292                        "def x3(): x2()\n\n"
1293                        "x3()\n"
1294                        "exit()\n")
1295
1296            for basic_repl in [True, False]:
1297                if basic_repl:
1298                    env["PYTHON_BASIC_REPL"] = "1"
1299                else:
1300                    env.pop("PYTHON_BASIC_REPL", None)
1301                with self.subTest(set_tracebacklimit=set_tracebacklimit,
1302                                  basic_repl=basic_repl):
1303                    output, exit_code = self.run_repl(commands, env=env)
1304                    if "can't use pyrepl" in output:
1305                        self.skipTest("pyrepl not available")
1306                    self.assertIn("in x1", output)
1307                    if set_tracebacklimit:
1308                        self.assertNotIn("in x2", output)
1309                        self.assertNotIn("in x3", output)
1310                        self.assertNotIn("in <module>", output)
1311                    else:
1312                        self.assertIn("in x2", output)
1313                        self.assertIn("in x3", output)
1314                        self.assertIn("in <module>", output)
1315
1316    def test_null_byte(self):
1317        output, exit_code = self.run_repl("\x00\nexit()\n")
1318        self.assertEqual(exit_code, 0)
1319        self.assertNotIn("TypeError", output)
1320
1321    def test_readline_history_file(self):
1322        # skip, if readline module is not available
1323        readline = import_module('readline')
1324        if readline.backend != "editline":
1325            self.skipTest("GNU readline is not affected by this issue")
1326
1327        hfile = tempfile.NamedTemporaryFile()
1328        self.addCleanup(unlink, hfile.name)
1329        env = os.environ.copy()
1330        env["PYTHON_HISTORY"] = hfile.name
1331
1332        env["PYTHON_BASIC_REPL"] = "1"
1333        output, exit_code = self.run_repl("spam \nexit()\n", env=env)
1334        self.assertEqual(exit_code, 0)
1335        self.assertIn("spam ", output)
1336        self.assertNotEqual(pathlib.Path(hfile.name).stat().st_size, 0)
1337        self.assertIn("spam\\040", pathlib.Path(hfile.name).read_text())
1338
1339        env.pop("PYTHON_BASIC_REPL", None)
1340        output, exit_code = self.run_repl("exit\n", env=env)
1341        self.assertEqual(exit_code, 0)
1342        self.assertNotIn("\\040", pathlib.Path(hfile.name).read_text())
1343
1344    def test_keyboard_interrupt_after_isearch(self):
1345        output, exit_code = self.run_repl(["\x12", "\x03", "exit"])
1346        self.assertEqual(exit_code, 0)
1347