1import itertools 2import os 3import pydoc 4import string 5import sys 6 7from contextlib import contextmanager 8from curtsies.formatstringarray import ( 9 fsarray, 10 assertFSArraysEqual, 11 assertFSArraysEqualIgnoringFormatting, 12) 13from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red 14from unittest import mock 15 16from bpython.curtsiesfrontend.events import RefreshRequestEvent 17from bpython import config, inspection 18from bpython.curtsiesfrontend.repl import BaseRepl 19from bpython.curtsiesfrontend import replpainter 20from bpython.curtsiesfrontend.repl import ( 21 INCONSISTENT_HISTORY_MSG, 22 CONTIGUITY_BROKEN_MSG, 23) 24from bpython.test import FixLanguageTestCase as TestCase, TEST_CONFIG 25 26 27def setup_config(): 28 config_struct = config.Config(TEST_CONFIG) 29 config_struct.cli_suggestion_width = 1 30 return config_struct 31 32 33class ClearEnviron(TestCase): 34 @classmethod 35 def setUpClass(cls): 36 cls.mock_environ = mock.patch.dict( 37 "os.environ", 38 { 39 "LC_ALL": os.environ.get("LC_ALL", "C.UTF-8"), 40 "LANG": os.environ.get("LANG", "C.UTF-8"), 41 }, 42 clear=True, 43 ) 44 cls.mock_environ.start() 45 TestCase.setUpClass() 46 47 @classmethod 48 def tearDownClass(cls): 49 cls.mock_environ.stop() 50 TestCase.tearDownClass() 51 52 53class CurtsiesPaintingTest(ClearEnviron): 54 def setUp(self): 55 class TestRepl(BaseRepl): 56 def _request_refresh(inner_self): 57 pass 58 59 self.repl = TestRepl(config=setup_config()) 60 self.repl.height, self.repl.width = (5, 10) 61 62 @property 63 def locals(self): 64 return self.repl.coderunner.interp.locals 65 66 def assert_paint(self, screen, cursor_row_col): 67 array, cursor_pos = self.repl.paint() 68 assertFSArraysEqual(array, screen) 69 self.assertEqual(cursor_pos, cursor_row_col) 70 71 def assert_paint_ignoring_formatting( 72 self, screen, cursor_row_col=None, **paint_kwargs 73 ): 74 array, cursor_pos = self.repl.paint(**paint_kwargs) 75 assertFSArraysEqualIgnoringFormatting(array, screen) 76 if cursor_row_col is not None: 77 self.assertEqual(cursor_pos, cursor_row_col) 78 79 def process_box_characters(self, screen): 80 if not self.repl.config.unicode_box or not config.supports_box_chars(): 81 return [ 82 line.replace("┌", "+") 83 .replace("└", "+") 84 .replace("┘", "+") 85 .replace("┐", "+") 86 .replace("─", "-") 87 for line in screen 88 ] 89 return screen 90 91 92class TestCurtsiesPaintingTest(CurtsiesPaintingTest): 93 def test_history_is_cleared(self): 94 self.assertEqual(self.repl.rl_history.entries, [""]) 95 96 97class TestCurtsiesPaintingSimple(CurtsiesPaintingTest): 98 def test_startup(self): 99 screen = fsarray([cyan(">>> "), cyan("Welcome to")]) 100 self.assert_paint(screen, (0, 4)) 101 102 def test_enter_text(self): 103 [self.repl.add_normal_character(c) for c in "1 + 1"] 104 screen = fsarray( 105 [ 106 cyan(">>> ") 107 + bold( 108 green("1") 109 + cyan(" ") 110 + yellow("+") 111 + cyan(" ") 112 + green("1") 113 ), 114 cyan("Welcome to"), 115 ] 116 ) 117 self.assert_paint(screen, (0, 9)) 118 119 def test_run_line(self): 120 try: 121 orig_stdout = sys.stdout 122 sys.stdout = self.repl.stdout 123 [self.repl.add_normal_character(c) for c in "1 + 1"] 124 self.repl.on_enter(new_code=False) 125 screen = fsarray([">>> 1 + 1", "2", "Welcome to"]) 126 self.assert_paint_ignoring_formatting(screen, (1, 1)) 127 finally: 128 sys.stdout = orig_stdout 129 130 def test_completion(self): 131 self.repl.height, self.repl.width = (5, 32) 132 self.repl.current_line = "an" 133 self.cursor_offset = 2 134 screen = self.process_box_characters( 135 [ 136 ">>> an", 137 "┌──────────────────────────────┐", 138 "│ and any( │", 139 "└──────────────────────────────┘", 140 "Welcome to bpython! Press <F1> f", 141 ] 142 if sys.version_info[:2] < (3, 10) 143 else [ 144 ">>> an", 145 "┌──────────────────────────────┐", 146 "│ and anext( any( │", 147 "└──────────────────────────────┘", 148 "Welcome to bpython! Press <F1> f", 149 ] 150 ) 151 self.assert_paint_ignoring_formatting(screen, (0, 4)) 152 153 def test_argspec(self): 154 def foo(x, y, z=10): 155 "docstring!" 156 pass 157 158 argspec = inspection.getfuncprops("foo", foo) 159 array = replpainter.formatted_argspec(argspec, 1, 30, setup_config()) 160 screen = [ 161 bold(cyan("foo")) 162 + cyan(":") 163 + cyan(" ") 164 + cyan("(") 165 + cyan("x") 166 + yellow(",") 167 + yellow(" ") 168 + bold(cyan("y")) 169 + yellow(",") 170 + yellow(" ") 171 + cyan("z") 172 + yellow("=") 173 + bold(cyan("10")) 174 + yellow(")") 175 ] 176 assertFSArraysEqual(fsarray(array), fsarray(screen)) 177 178 def test_formatted_docstring(self): 179 actual = replpainter.formatted_docstring( 180 "Returns the results\n\n" "Also has side effects", 181 40, 182 config=setup_config(), 183 ) 184 expected = fsarray(["Returns the results", "", "Also has side effects"]) 185 assertFSArraysEqualIgnoringFormatting(actual, expected) 186 187 def test_unicode_docstrings(self): 188 "A bit of a special case in Python 2" 189 # issue 653 190 191 def foo(): 192 "åß∂ƒ" 193 194 actual = replpainter.formatted_docstring( 195 foo.__doc__, 40, config=setup_config() 196 ) 197 expected = fsarray(["åß∂ƒ"]) 198 assertFSArraysEqualIgnoringFormatting(actual, expected) 199 200 def test_nonsense_docstrings(self): 201 for docstring in [ 202 123, 203 {}, 204 [], 205 ]: 206 try: 207 replpainter.formatted_docstring( 208 docstring, 40, config=setup_config() 209 ) 210 except Exception: 211 self.fail(f"bad docstring caused crash: {docstring!r}") 212 213 def test_weird_boto_docstrings(self): 214 # Boto does something like this. 215 # botocore: botocore/docs/docstring.py 216 class WeirdDocstring(str): 217 # a mighty hack. See botocore/docs/docstring.py 218 def expandtabs(self, tabsize=8): 219 return "asdfåß∂ƒ".expandtabs(tabsize) 220 221 def foo(): 222 pass 223 224 foo.__doc__ = WeirdDocstring() 225 wd = pydoc.getdoc(foo) 226 actual = replpainter.formatted_docstring(wd, 40, config=setup_config()) 227 expected = fsarray(["asdfåß∂ƒ"]) 228 assertFSArraysEqualIgnoringFormatting(actual, expected) 229 230 def test_paint_lasts_events(self): 231 actual = replpainter.paint_last_events( 232 4, 100, ["a", "b", "c"], config=setup_config() 233 ) 234 if config.supports_box_chars(): 235 expected = fsarray(["┌─┐", "│c│", "│b│", "└─┘"]) 236 else: 237 expected = fsarray(["+-+", "|c|", "|b|", "+-+"]) 238 239 assertFSArraysEqualIgnoringFormatting(actual, expected) 240 241 242@contextmanager 243def output_to_repl(repl): 244 old_out, old_err = sys.stdout, sys.stderr 245 try: 246 sys.stdout, sys.stderr = repl.stdout, repl.stderr 247 yield 248 finally: 249 sys.stdout, sys.stderr = old_out, old_err 250 251 252class HigherLevelCurtsiesPaintingTest(CurtsiesPaintingTest): 253 def refresh(self): 254 self.refresh_requests.append(RefreshRequestEvent()) 255 256 def send_refreshes(self): 257 while self.refresh_requests: 258 self.repl.process_event(self.refresh_requests.pop()) 259 _, _ = self.repl.paint() 260 261 def enter(self, line=None): 262 """Enter a line of text, avoiding autocompletion windows 263 264 autocomplete could still happen if the entered line has 265 autocompletion that would happen then, but intermediate 266 stages won't happen""" 267 if line is not None: 268 self.repl._set_cursor_offset(len(line), update_completion=False) 269 self.repl.current_line = line 270 with output_to_repl(self.repl): 271 self.repl.on_enter(new_code=False) 272 self.assertEqual(self.repl.rl_history.entries, [""]) 273 self.send_refreshes() 274 275 def undo(self): 276 with output_to_repl(self.repl): 277 self.repl.undo() 278 self.send_refreshes() 279 280 def setUp(self): 281 self.refresh_requests = [] 282 283 class TestRepl(BaseRepl): 284 def _request_refresh(inner_self): 285 self.refresh() 286 287 self.repl = TestRepl(banner="", config=setup_config()) 288 self.repl.height, self.repl.width = (5, 32) 289 290 def send_key(self, key): 291 self.repl.process_event("<SPACE>" if key == " " else key) 292 self.repl.paint() # has some side effects we need to be wary of 293 294 295class TestWidthAwareness(HigherLevelCurtsiesPaintingTest): 296 def test_cursor_position_with_fullwidth_char(self): 297 self.repl.add_normal_character("間") 298 299 cursor_pos = self.repl.paint()[1] 300 self.assertEqual(cursor_pos, (0, 6)) 301 302 def test_cursor_position_with_padding_char(self): 303 # odd numbered so fullwidth chars don't wrap evenly 304 self.repl.width = 11 305 [self.repl.add_normal_character(c) for c in "width"] 306 307 cursor_pos = self.repl.paint()[1] 308 self.assertEqual(cursor_pos, (1, 4)) 309 310 def test_display_of_padding_chars(self): 311 self.repl.width = 11 312 [self.repl.add_normal_character(c) for c in "width"] 313 314 self.enter() 315 expected = [">>> wid ", "th"] # <--- note the added trailing space 316 result = [d.s for d in self.repl.display_lines[0:2]] 317 self.assertEqual(result, expected) 318 319 320class TestCurtsiesRewindRedraw(HigherLevelCurtsiesPaintingTest): 321 def test_rewind(self): 322 self.repl.current_line = "1 + 1" 323 self.enter() 324 screen = [">>> 1 + 1", "2", ">>> "] 325 self.assert_paint_ignoring_formatting(screen, (2, 4)) 326 self.repl.undo() 327 screen = [">>> "] 328 self.assert_paint_ignoring_formatting(screen, (0, 4)) 329 330 def test_rewind_contiguity_loss(self): 331 self.enter("1 + 1") 332 self.enter("2 + 2") 333 self.enter("def foo(x):") 334 self.repl.current_line = " return x + 1" 335 screen = [ 336 ">>> 1 + 1", 337 "2", 338 ">>> 2 + 2", 339 "4", 340 ">>> def foo(x):", 341 "... return x + 1", 342 ] 343 self.assert_paint_ignoring_formatting(screen, (5, 8)) 344 self.repl.scroll_offset = 1 345 self.assert_paint_ignoring_formatting(screen[1:], (4, 8)) 346 self.undo() 347 screen = ["2", ">>> 2 + 2", "4", ">>> "] 348 self.assert_paint_ignoring_formatting(screen, (3, 4)) 349 self.undo() 350 screen = ["2", ">>> "] 351 self.assert_paint_ignoring_formatting(screen, (1, 4)) 352 self.undo() 353 screen = [ 354 CONTIGUITY_BROKEN_MSG[: self.repl.width], 355 ">>> ", 356 "", 357 "", 358 "", 359 " ", 360 ] # TODO why is that there? Necessary? 361 self.assert_paint_ignoring_formatting(screen, (1, 4)) 362 screen = [">>> "] 363 self.assert_paint_ignoring_formatting(screen, (0, 4)) 364 365 def test_inconsistent_history_doesnt_happen_if_onscreen(self): 366 self.enter("1 + 1") 367 screen = [">>> 1 + 1", "2", ">>> "] 368 self.assert_paint_ignoring_formatting(screen, (2, 4)) 369 self.enter("2 + 2") 370 screen = [">>> 1 + 1", "2", ">>> 2 + 2", "4", ">>> "] 371 self.assert_paint_ignoring_formatting(screen, (4, 4)) 372 self.repl.display_lines[0] = self.repl.display_lines[0] * 2 373 self.undo() 374 screen = [">>> 1 + 1", "2", ">>> "] 375 self.assert_paint_ignoring_formatting(screen, (2, 4)) 376 377 def test_rewind_inconsistent_history(self): 378 self.enter("1 + 1") 379 self.enter("2 + 2") 380 self.enter("3 + 3") 381 screen = [">>> 1 + 1", "2", ">>> 2 + 2", "4", ">>> 3 + 3", "6", ">>> "] 382 self.assert_paint_ignoring_formatting(screen, (6, 4)) 383 self.repl.scroll_offset += len(screen) - self.repl.height 384 self.assert_paint_ignoring_formatting(screen[2:], (4, 4)) 385 self.repl.display_lines[0] = self.repl.display_lines[0] * 2 386 self.undo() 387 screen = [ 388 INCONSISTENT_HISTORY_MSG[: self.repl.width], 389 ">>> 2 + 2", 390 "4", 391 ">>> ", 392 "", 393 " ", 394 ] 395 self.assert_paint_ignoring_formatting(screen, (3, 4)) 396 self.repl.scroll_offset += len(screen) - self.repl.height 397 self.assert_paint_ignoring_formatting(screen[1:-2], (2, 4)) 398 self.assert_paint_ignoring_formatting(screen[1:-2], (2, 4)) 399 400 def test_rewind_inconsistent_history_more_lines_same_screen(self): 401 self.repl.width = 60 402 sys.a = 5 403 self.enter("import sys") 404 self.enter("for i in range(sys.a):") 405 self.enter(" print(sys.a)") 406 self.enter("") 407 self.enter("1 + 1") 408 self.enter("2 + 2") 409 screen = [ 410 ">>> import sys", 411 ">>> for i in range(sys.a):", 412 "... print(sys.a)", 413 "... ", 414 "5", 415 "5", 416 "5", 417 "5", 418 "5", 419 ">>> 1 + 1", 420 "2", 421 ">>> 2 + 2", 422 "4", 423 ">>> ", 424 ] 425 self.assert_paint_ignoring_formatting(screen, (13, 4)) 426 self.repl.scroll_offset += len(screen) - self.repl.height 427 self.assert_paint_ignoring_formatting(screen[9:], (4, 4)) 428 sys.a = 6 429 self.undo() 430 screen = [ 431 INCONSISTENT_HISTORY_MSG[: self.repl.width], 432 "6", 433 # everything will jump down a line - that's perfectly 434 # reasonable 435 ">>> 1 + 1", 436 "2", 437 ">>> ", 438 " ", 439 ] 440 self.assert_paint_ignoring_formatting(screen, (4, 4)) 441 self.repl.scroll_offset += len(screen) - self.repl.height 442 self.assert_paint_ignoring_formatting(screen[1:-1], (3, 4)) 443 444 def test_rewind_inconsistent_history_more_lines_lower_screen(self): 445 self.repl.width = 60 446 sys.a = 5 447 self.enter("import sys") 448 self.enter("for i in range(sys.a):") 449 self.enter(" print(sys.a)") 450 self.enter("") 451 self.enter("1 + 1") 452 self.enter("2 + 2") 453 screen = [ 454 ">>> import sys", 455 ">>> for i in range(sys.a):", 456 "... print(sys.a)", 457 "... ", 458 "5", 459 "5", 460 "5", 461 "5", 462 "5", 463 ">>> 1 + 1", 464 "2", 465 ">>> 2 + 2", 466 "4", 467 ">>> ", 468 ] 469 self.assert_paint_ignoring_formatting(screen, (13, 4)) 470 self.repl.scroll_offset += len(screen) - self.repl.height 471 self.assert_paint_ignoring_formatting(screen[9:], (4, 4)) 472 sys.a = 8 473 self.undo() 474 screen = [ 475 INCONSISTENT_HISTORY_MSG[: self.repl.width], 476 "8", 477 "8", 478 "8", 479 ">>> 1 + 1", 480 "2", 481 ">>> ", 482 ] 483 self.assert_paint_ignoring_formatting(screen) 484 self.repl.scroll_offset += len(screen) - self.repl.height 485 self.assert_paint_ignoring_formatting(screen[-5:]) 486 487 def test_rewind_inconsistent_history_more_lines_raise_screen(self): 488 self.repl.width = 60 489 sys.a = 5 490 self.enter("import sys") 491 self.enter("for i in range(sys.a):") 492 self.enter(" print(sys.a)") 493 self.enter("") 494 self.enter("1 + 1") 495 self.enter("2 + 2") 496 screen = [ 497 ">>> import sys", 498 ">>> for i in range(sys.a):", 499 "... print(sys.a)", 500 "... ", 501 "5", 502 "5", 503 "5", 504 "5", 505 "5", 506 ">>> 1 + 1", 507 "2", 508 ">>> 2 + 2", 509 "4", 510 ">>> ", 511 ] 512 self.assert_paint_ignoring_formatting(screen, (13, 4)) 513 self.repl.scroll_offset += len(screen) - self.repl.height 514 self.assert_paint_ignoring_formatting(screen[9:], (4, 4)) 515 sys.a = 1 516 self.undo() 517 screen = [ 518 INCONSISTENT_HISTORY_MSG[: self.repl.width], 519 "1", 520 ">>> 1 + 1", 521 "2", 522 ">>> ", 523 " ", 524 ] 525 self.assert_paint_ignoring_formatting(screen) 526 self.repl.scroll_offset += len(screen) - self.repl.height 527 self.assert_paint_ignoring_formatting(screen[1:-1]) 528 529 def test_rewind_history_not_quite_inconsistent(self): 530 self.repl.width = 50 531 sys.a = 5 532 self.enter("for i in range(__import__('sys').a):") 533 self.enter(" print(i)") 534 self.enter("") 535 self.enter("1 + 1") 536 self.enter("2 + 2") 537 screen = [ 538 ">>> for i in range(__import__('sys').a):", 539 "... print(i)", 540 "... ", 541 "0", 542 "1", 543 "2", 544 "3", 545 "4", 546 ">>> 1 + 1", 547 "2", 548 ">>> 2 + 2", 549 "4", 550 ">>> ", 551 ] 552 self.assert_paint_ignoring_formatting(screen, (12, 4)) 553 self.repl.scroll_offset += len(screen) - self.repl.height 554 self.assert_paint_ignoring_formatting(screen[8:], (4, 4)) 555 sys.a = 6 556 self.undo() 557 screen = [ 558 "5", 559 # everything will jump down a line - that's perfectly 560 # reasonable 561 ">>> 1 + 1", 562 "2", 563 ">>> ", 564 ] 565 self.assert_paint_ignoring_formatting(screen, (3, 4)) 566 567 def test_rewind_barely_consistent(self): 568 self.enter("1 + 1") 569 self.enter("2 + 2") 570 self.enter("3 + 3") 571 screen = [">>> 1 + 1", "2", ">>> 2 + 2", "4", ">>> 3 + 3", "6", ">>> "] 572 self.assert_paint_ignoring_formatting(screen, (6, 4)) 573 self.repl.scroll_offset += len(screen) - self.repl.height 574 self.assert_paint_ignoring_formatting(screen[2:], (4, 4)) 575 self.repl.display_lines[2] = self.repl.display_lines[2] * 2 576 self.undo() 577 screen = [">>> 2 + 2", "4", ">>> "] 578 self.assert_paint_ignoring_formatting(screen, (2, 4)) 579 580 def test_clear_screen(self): 581 self.enter("1 + 1") 582 self.enter("2 + 2") 583 screen = [">>> 1 + 1", "2", ">>> 2 + 2", "4", ">>> "] 584 self.assert_paint_ignoring_formatting(screen, (4, 4)) 585 self.repl.request_paint_to_clear_screen = True 586 screen = [">>> 1 + 1", "2", ">>> 2 + 2", "4", ">>> ", "", "", "", ""] 587 self.assert_paint_ignoring_formatting(screen, (4, 4)) 588 589 def test_scroll_down_while_banner_visible(self): 590 self.repl.status_bar.message("STATUS_BAR") 591 self.enter("1 + 1") 592 self.enter("2 + 2") 593 screen = [ 594 ">>> 1 + 1", 595 "2", 596 ">>> 2 + 2", 597 "4", 598 ">>> ", 599 "STATUS_BAR ", 600 ] 601 self.assert_paint_ignoring_formatting(screen, (4, 4)) 602 self.repl.scroll_offset += len(screen) - self.repl.height 603 self.assert_paint_ignoring_formatting(screen[1:], (3, 4)) 604 605 def test_clear_screen_while_banner_visible(self): 606 self.repl.status_bar.message("STATUS_BAR") 607 self.enter("1 + 1") 608 self.enter("2 + 2") 609 screen = [ 610 ">>> 1 + 1", 611 "2", 612 ">>> 2 + 2", 613 "4", 614 ">>> ", 615 "STATUS_BAR ", 616 ] 617 self.assert_paint_ignoring_formatting(screen, (4, 4)) 618 self.repl.scroll_offset += len(screen) - self.repl.height 619 self.assert_paint_ignoring_formatting(screen[1:], (3, 4)) 620 621 self.repl.request_paint_to_clear_screen = True 622 screen = [ 623 "2", 624 ">>> 2 + 2", 625 "4", 626 ">>> ", 627 "", 628 "", 629 "", 630 "STATUS_BAR ", 631 ] 632 self.assert_paint_ignoring_formatting(screen, (3, 4)) 633 634 def test_cursor_stays_at_bottom_of_screen(self): 635 """infobox showing up during intermediate render was causing this to 636 fail, #371""" 637 self.repl.width = 50 638 self.repl.current_line = "__import__('random').__name__" 639 with output_to_repl(self.repl): 640 self.repl.on_enter(new_code=False) 641 screen = [">>> __import__('random').__name__", "'random'"] 642 self.assert_paint_ignoring_formatting(screen) 643 644 with output_to_repl(self.repl): 645 self.repl.process_event(self.refresh_requests.pop()) 646 screen = [">>> __import__('random').__name__", "'random'", ""] 647 self.assert_paint_ignoring_formatting(screen) 648 649 with output_to_repl(self.repl): 650 self.repl.process_event(self.refresh_requests.pop()) 651 screen = [">>> __import__('random').__name__", "'random'", ">>> "] 652 self.assert_paint_ignoring_formatting(screen, (2, 4)) 653 654 def test_unhighlight_paren_bugs(self): 655 """two previous bugs, parent didn't highlight until next render 656 and paren didn't unhighlight until enter""" 657 self.repl.width = 32 658 self.assertEqual(self.repl.rl_history.entries, [""]) 659 self.enter("(") 660 self.assertEqual(self.repl.rl_history.entries, [""]) 661 screen = [">>> (", "... "] 662 self.assertEqual(self.repl.rl_history.entries, [""]) 663 self.assert_paint_ignoring_formatting(screen) 664 self.assertEqual(self.repl.rl_history.entries, [""]) 665 666 with output_to_repl(self.repl): 667 self.assertEqual(self.repl.rl_history.entries, [""]) 668 self.repl.process_event(")") 669 self.assertEqual(self.repl.rl_history.entries, [""]) 670 screen = fsarray( 671 [ 672 cyan(">>> ") + on_magenta(bold(red("("))), 673 green("... ") + on_magenta(bold(red(")"))), 674 ], 675 width=32, 676 ) 677 self.assert_paint(screen, (1, 5)) 678 679 with output_to_repl(self.repl): 680 self.repl.process_event(" ") 681 screen = fsarray( 682 [ 683 cyan(">>> ") + yellow("("), 684 green("... ") + yellow(")") + bold(cyan(" ")), 685 ], 686 width=32, 687 ) 688 self.assert_paint(screen, (1, 6)) 689 690 def test_472(self): 691 [self.send_key(c) for c in "(1, 2, 3)"] 692 with output_to_repl(self.repl): 693 self.send_key("\n") 694 self.send_refreshes() 695 self.send_key("<UP>") 696 self.repl.paint() 697 [self.send_key("<LEFT>") for _ in range(4)] 698 self.send_key("<BACKSPACE>") 699 self.send_key("4") 700 self.repl.on_enter() 701 self.send_refreshes() 702 screen = [ 703 ">>> (1, 2, 3)", 704 "(1, 2, 3)", 705 ">>> (1, 4, 3)", 706 "(1, 4, 3)", 707 ">>> ", 708 ] 709 self.assert_paint_ignoring_formatting(screen, (4, 4)) 710 711 712def completion_target(num_names, chars_in_first_name=1): 713 class Class: 714 pass 715 716 if chars_in_first_name < 1: 717 raise ValueError("need at least one char in each name") 718 elif chars_in_first_name == 1 and num_names > len(string.ascii_letters): 719 raise ValueError("need more chars to make so many names") 720 721 names = gen_names() 722 if num_names > 0: 723 setattr(Class, "a" * chars_in_first_name, 1) 724 next(names) # use the above instead of first name 725 for _, name in zip(range(num_names - 1), names): 726 setattr(Class, name, 0) 727 728 return Class() 729 730 731def gen_names(): 732 for letters in itertools.chain( 733 itertools.combinations_with_replacement(string.ascii_letters, 1), 734 itertools.combinations_with_replacement(string.ascii_letters, 2), 735 ): 736 yield "".join(letters) 737 738 739class TestCompletionHelpers(TestCase): 740 def test_gen_names(self): 741 self.assertEqual( 742 list(zip([1, 2, 3], gen_names())), [(1, "a"), (2, "b"), (3, "c")] 743 ) 744 745 def test_completion_target(self): 746 target = completion_target(14) 747 self.assertEqual( 748 len([x for x in dir(target) if not x.startswith("_")]), 14 749 ) 750 751 752class TestCurtsiesInfoboxPaint(HigherLevelCurtsiesPaintingTest): 753 def test_simple(self): 754 self.repl.width, self.repl.height = (20, 30) 755 self.locals["abc"] = completion_target(3, 50) 756 self.repl.current_line = "abc" 757 self.repl.cursor_offset = 3 758 self.repl.process_event(".") 759 screen = self.process_box_characters( 760 [ 761 ">>> abc.", 762 "┌──────────────────┐", 763 "│ aaaaaaaaaaaaaaaa │", 764 "│ b │", 765 "│ c │", 766 "└──────────────────┘", 767 ] 768 ) 769 self.assert_paint_ignoring_formatting(screen, (0, 8)) 770 771 def test_fill_screen(self): 772 self.repl.width, self.repl.height = (20, 15) 773 self.locals["abc"] = completion_target(20, 100) 774 self.repl.current_line = "abc" 775 self.repl.cursor_offset = 3 776 self.repl.process_event(".") 777 screen = self.process_box_characters( 778 [ 779 ">>> abc.", 780 "┌──────────────────┐", 781 "│ aaaaaaaaaaaaaaaa │", 782 "│ b │", 783 "│ c │", 784 "│ d │", 785 "│ e │", 786 "│ f │", 787 "│ g │", 788 "│ h │", 789 "│ i │", 790 "│ j │", 791 "│ k │", 792 "│ l │", 793 "└──────────────────┘", 794 ] 795 ) 796 self.assert_paint_ignoring_formatting(screen, (0, 8)) 797 798 def test_lower_on_screen(self): 799 self.repl.get_top_usable_line = lambda: 10 # halfway down terminal 800 self.repl.width, self.repl.height = (20, 15) 801 self.locals["abc"] = completion_target(20, 100) 802 self.repl.current_line = "abc" 803 self.repl.cursor_offset = 3 804 self.repl.process_event(".") 805 screen = self.process_box_characters( 806 [ 807 ">>> abc.", 808 "┌──────────────────┐", 809 "│ aaaaaaaaaaaaaaaa │", 810 "│ b │", 811 "│ c │", 812 "│ d │", 813 "│ e │", 814 "│ f │", 815 "│ g │", 816 "│ h │", 817 "│ i │", 818 "│ j │", 819 "│ k │", 820 "│ l │", 821 "└──────────────────┘", 822 ] 823 ) 824 # behavior before issue #466 825 self.assert_paint_ignoring_formatting( 826 screen, try_preserve_history_height=0 827 ) 828 self.assert_paint_ignoring_formatting(screen, min_infobox_height=100) 829 # behavior after issue #466 830 screen = self.process_box_characters( 831 [ 832 ">>> abc.", 833 "┌──────────────────┐", 834 "│ aaaaaaaaaaaaaaaa │", 835 "│ b │", 836 "│ c │", 837 "└──────────────────┘", 838 ] 839 ) 840 self.assert_paint_ignoring_formatting(screen) 841 842 def test_at_bottom_of_screen(self): 843 self.repl.get_top_usable_line = lambda: 17 # two lines from bottom 844 self.repl.width, self.repl.height = (20, 15) 845 self.locals["abc"] = completion_target(20, 100) 846 self.repl.current_line = "abc" 847 self.repl.cursor_offset = 3 848 self.repl.process_event(".") 849 screen = self.process_box_characters( 850 [ 851 ">>> abc.", 852 "┌──────────────────┐", 853 "│ aaaaaaaaaaaaaaaa │", 854 "│ b │", 855 "│ c │", 856 "│ d │", 857 "│ e │", 858 "│ f │", 859 "│ g │", 860 "│ h │", 861 "│ i │", 862 "│ j │", 863 "│ k │", 864 "│ l │", 865 "└──────────────────┘", 866 ] 867 ) 868 # behavior before issue #466 869 self.assert_paint_ignoring_formatting( 870 screen, try_preserve_history_height=0 871 ) 872 self.assert_paint_ignoring_formatting(screen, min_infobox_height=100) 873 # behavior after issue #466 874 screen = self.process_box_characters( 875 [ 876 ">>> abc.", 877 "┌──────────────────┐", 878 "│ aaaaaaaaaaaaaaaa │", 879 "│ b │", 880 "│ c │", 881 "└──────────────────┘", 882 ] 883 ) 884 self.assert_paint_ignoring_formatting(screen) 885