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