Coverage for idle_test/test_sidebar.py: 22%

519 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1"""Test sidebar, coverage 85%""" 

2from textwrap import dedent 

3import sys 

4 

5from itertools import chain 

6import unittest 

7import unittest.mock 

8from test.support import requires, swap_attr 

9from test import support 

10import tkinter as tk 

11from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop 

12 

13from idlelib.delegator import Delegator 

14from idlelib.editor import fixwordbreaks 

15from idlelib.percolator import Percolator 

16import idlelib.pyshell 

17from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList 

18from idlelib.run import fix_scaling 

19import idlelib.sidebar 

20from idlelib.sidebar import get_end_linenumber, get_lineno 

21 

22 

23class Dummy_editwin: 

24 def __init__(self, text): 

25 self.text = text 

26 self.text_frame = self.text.master 

27 self.per = Percolator(text) 

28 self.undo = Delegator() 

29 self.per.insertfilter(self.undo) 

30 

31 def setvar(self, name, value): 

32 pass 

33 

34 def getlineno(self, index): 

35 return int(float(self.text.index(index))) 

36 

37 

38class LineNumbersTest(unittest.TestCase): 

39 

40 @classmethod 

41 def setUpClass(cls): 

42 requires('gui') 

43 cls.root = tk.Tk() 

44 cls.root.withdraw() 

45 

46 cls.text_frame = tk.Frame(cls.root) 

47 cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 

48 cls.text_frame.rowconfigure(1, weight=1) 

49 cls.text_frame.columnconfigure(1, weight=1) 

50 

51 cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE) 

52 cls.text.grid(row=1, column=1, sticky=tk.NSEW) 

53 

54 cls.editwin = Dummy_editwin(cls.text) 

55 cls.editwin.vbar = tk.Scrollbar(cls.text_frame) 

56 

57 @classmethod 

58 def tearDownClass(cls): 

59 cls.editwin.per.close() 

60 cls.root.update() 

61 cls.root.destroy() 

62 del cls.text, cls.text_frame, cls.editwin, cls.root 

63 

64 def setUp(self): 

65 self.linenumber = idlelib.sidebar.LineNumbers(self.editwin) 

66 

67 self.highlight_cfg = {"background": '#abcdef', 

68 "foreground": '#123456'} 

69 orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight 

70 def mock_idleconf_GetHighlight(theme, element): 

71 if element == 'linenumber': 

72 return self.highlight_cfg 

73 return orig_idleConf_GetHighlight(theme, element) 

74 GetHighlight_patcher = unittest.mock.patch.object( 

75 idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) 

76 GetHighlight_patcher.start() 

77 self.addCleanup(GetHighlight_patcher.stop) 

78 

79 self.font_override = 'TkFixedFont' 

80 def mock_idleconf_GetFont(root, configType, section): 

81 return self.font_override 

82 GetFont_patcher = unittest.mock.patch.object( 

83 idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) 

84 GetFont_patcher.start() 

85 self.addCleanup(GetFont_patcher.stop) 

86 

87 def tearDown(self): 

88 self.text.delete('1.0', 'end') 

89 

90 def get_selection(self): 

91 return tuple(map(str, self.text.tag_ranges('sel'))) 

92 

93 def get_line_screen_position(self, line): 

94 bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c') 

95 x = bbox[0] + 2 

96 y = bbox[1] + 2 

97 return x, y 

98 

99 def assert_state_disabled(self): 

100 state = self.linenumber.sidebar_text.config()['state'] 

101 self.assertEqual(state[-1], tk.DISABLED) 

102 

103 def get_sidebar_text_contents(self): 

104 return self.linenumber.sidebar_text.get('1.0', tk.END) 

105 

106 def assert_sidebar_n_lines(self, n_lines): 

107 expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), [''])) 

108 self.assertEqual(self.get_sidebar_text_contents(), expected) 

109 

110 def assert_text_equals(self, expected): 

111 return self.assertEqual(self.text.get('1.0', 'end'), expected) 

112 

113 def test_init_empty(self): 

114 self.assert_sidebar_n_lines(1) 

115 

116 def test_init_not_empty(self): 

117 self.text.insert('insert', 'foo bar\n'*3) 

118 self.assert_text_equals('foo bar\n'*3 + '\n') 

119 self.assert_sidebar_n_lines(4) 

120 

121 def test_toggle_linenumbering(self): 

122 self.assertEqual(self.linenumber.is_shown, False) 

123 self.linenumber.show_sidebar() 

124 self.assertEqual(self.linenumber.is_shown, True) 

125 self.linenumber.hide_sidebar() 

126 self.assertEqual(self.linenumber.is_shown, False) 

127 self.linenumber.hide_sidebar() 

128 self.assertEqual(self.linenumber.is_shown, False) 

129 self.linenumber.show_sidebar() 

130 self.assertEqual(self.linenumber.is_shown, True) 

131 self.linenumber.show_sidebar() 

132 self.assertEqual(self.linenumber.is_shown, True) 

133 

134 def test_insert(self): 

135 self.text.insert('insert', 'foobar') 

136 self.assert_text_equals('foobar\n') 

137 self.assert_sidebar_n_lines(1) 

138 self.assert_state_disabled() 

139 

140 self.text.insert('insert', '\nfoo') 

141 self.assert_text_equals('foobar\nfoo\n') 

142 self.assert_sidebar_n_lines(2) 

143 self.assert_state_disabled() 

144 

145 self.text.insert('insert', 'hello\n'*2) 

146 self.assert_text_equals('foobar\nfoohello\nhello\n\n') 

147 self.assert_sidebar_n_lines(4) 

148 self.assert_state_disabled() 

149 

150 self.text.insert('insert', '\nworld') 

151 self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n') 

152 self.assert_sidebar_n_lines(5) 

153 self.assert_state_disabled() 

154 

155 def test_delete(self): 

156 self.text.insert('insert', 'foobar') 

157 self.assert_text_equals('foobar\n') 

158 self.text.delete('1.1', '1.3') 

159 self.assert_text_equals('fbar\n') 

160 self.assert_sidebar_n_lines(1) 

161 self.assert_state_disabled() 

162 

163 self.text.insert('insert', 'foo\n'*2) 

164 self.assert_text_equals('fbarfoo\nfoo\n\n') 

165 self.assert_sidebar_n_lines(3) 

166 self.assert_state_disabled() 

167 

168 # Deleting up to "2.end" doesn't delete the final newline. 

169 self.text.delete('2.0', '2.end') 

170 self.assert_text_equals('fbarfoo\n\n\n') 

171 self.assert_sidebar_n_lines(3) 

172 self.assert_state_disabled() 

173 

174 self.text.delete('1.3', 'end') 

175 self.assert_text_equals('fba\n') 

176 self.assert_sidebar_n_lines(1) 

177 self.assert_state_disabled() 

178 

179 # Text widgets always keep a single '\n' character at the end. 

180 self.text.delete('1.0', 'end') 

181 self.assert_text_equals('\n') 

182 self.assert_sidebar_n_lines(1) 

183 self.assert_state_disabled() 

184 

185 def test_sidebar_text_width(self): 

186 """ 

187 Test that linenumber text widget is always at the minimum 

188 width 

189 """ 

190 def get_width(): 

191 return self.linenumber.sidebar_text.config()['width'][-1] 

192 

193 self.assert_sidebar_n_lines(1) 

194 self.assertEqual(get_width(), 1) 

195 

196 self.text.insert('insert', 'foo') 

197 self.assert_sidebar_n_lines(1) 

198 self.assertEqual(get_width(), 1) 

199 

200 self.text.insert('insert', 'foo\n'*8) 

201 self.assert_sidebar_n_lines(9) 

202 self.assertEqual(get_width(), 1) 

203 

204 self.text.insert('insert', 'foo\n') 

205 self.assert_sidebar_n_lines(10) 

206 self.assertEqual(get_width(), 2) 

207 

208 self.text.insert('insert', 'foo\n') 

209 self.assert_sidebar_n_lines(11) 

210 self.assertEqual(get_width(), 2) 

211 

212 self.text.delete('insert -1l linestart', 'insert linestart') 

213 self.assert_sidebar_n_lines(10) 

214 self.assertEqual(get_width(), 2) 

215 

216 self.text.delete('insert -1l linestart', 'insert linestart') 

217 self.assert_sidebar_n_lines(9) 

218 self.assertEqual(get_width(), 1) 

219 

220 self.text.insert('insert', 'foo\n'*90) 

221 self.assert_sidebar_n_lines(99) 

222 self.assertEqual(get_width(), 2) 

223 

224 self.text.insert('insert', 'foo\n') 

225 self.assert_sidebar_n_lines(100) 

226 self.assertEqual(get_width(), 3) 

227 

228 self.text.insert('insert', 'foo\n') 

229 self.assert_sidebar_n_lines(101) 

230 self.assertEqual(get_width(), 3) 

231 

232 self.text.delete('insert -1l linestart', 'insert linestart') 

233 self.assert_sidebar_n_lines(100) 

234 self.assertEqual(get_width(), 3) 

235 

236 self.text.delete('insert -1l linestart', 'insert linestart') 

237 self.assert_sidebar_n_lines(99) 

238 self.assertEqual(get_width(), 2) 

239 

240 self.text.delete('50.0 -1c', 'end -1c') 

241 self.assert_sidebar_n_lines(49) 

242 self.assertEqual(get_width(), 2) 

243 

244 self.text.delete('5.0 -1c', 'end -1c') 

245 self.assert_sidebar_n_lines(4) 

246 self.assertEqual(get_width(), 1) 

247 

248 # Text widgets always keep a single '\n' character at the end. 

249 self.text.delete('1.0', 'end -1c') 

250 self.assert_sidebar_n_lines(1) 

251 self.assertEqual(get_width(), 1) 

252 

253 # The following tests are temporarily disabled due to relying on 

254 # simulated user input and inspecting which text is selected, which 

255 # are fragile and can fail when several GUI tests are run in parallel 

256 # or when the windows created by the test lose focus. 

257 # 

258 # TODO: Re-work these tests or remove them from the test suite. 

259 

260 @unittest.skip('test disabled') 

261 def test_click_selection(self): 

262 self.linenumber.show_sidebar() 

263 self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') 

264 self.root.update() 

265 

266 # Click on the second line. 

267 x, y = self.get_line_screen_position(2) 

268 self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y) 

269 self.linenumber.sidebar_text.update() 

270 self.root.update() 

271 

272 self.assertEqual(self.get_selection(), ('2.0', '3.0')) 

273 

274 def simulate_drag(self, start_line, end_line): 

275 start_x, start_y = self.get_line_screen_position(start_line) 

276 end_x, end_y = self.get_line_screen_position(end_line) 

277 

278 self.linenumber.sidebar_text.event_generate('<Button-1>', 

279 x=start_x, y=start_y) 

280 self.root.update() 

281 

282 def lerp(a, b, steps): 

283 """linearly interpolate from a to b (inclusive) in equal steps""" 

284 last_step = steps - 1 

285 for i in range(steps): 

286 yield ((last_step - i) / last_step) * a + (i / last_step) * b 

287 

288 for x, y in zip( 

289 map(int, lerp(start_x, end_x, steps=11)), 

290 map(int, lerp(start_y, end_y, steps=11)), 

291 ): 

292 self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y) 

293 self.root.update() 

294 

295 self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>', 

296 x=end_x, y=end_y) 

297 self.root.update() 

298 

299 @unittest.skip('test disabled') 

300 def test_drag_selection_down(self): 

301 self.linenumber.show_sidebar() 

302 self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') 

303 self.root.update() 

304 

305 # Drag from the second line to the fourth line. 

306 self.simulate_drag(2, 4) 

307 self.assertEqual(self.get_selection(), ('2.0', '5.0')) 

308 

309 @unittest.skip('test disabled') 

310 def test_drag_selection_up(self): 

311 self.linenumber.show_sidebar() 

312 self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') 

313 self.root.update() 

314 

315 # Drag from the fourth line to the second line. 

316 self.simulate_drag(4, 2) 

317 self.assertEqual(self.get_selection(), ('2.0', '5.0')) 

318 

319 def test_scroll(self): 

320 self.linenumber.show_sidebar() 

321 self.text.insert('1.0', 'line\n' * 100) 

322 self.root.update() 

323 

324 # Scroll down 10 lines. 

325 self.text.yview_scroll(10, 'unit') 

326 self.root.update() 

327 self.assertEqual(self.text.index('@0,0'), '11.0') 

328 self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') 

329 

330 # Generate a mouse-wheel event and make sure it scrolled up or down. 

331 # The meaning of the "delta" is OS-dependant, so this just checks for 

332 # any change. 

333 self.linenumber.sidebar_text.event_generate('<MouseWheel>', 

334 x=0, y=0, 

335 delta=10) 

336 self.root.update() 

337 self.assertNotEqual(self.text.index('@0,0'), '11.0') 

338 self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') 

339 

340 def test_font(self): 

341 ln = self.linenumber 

342 

343 orig_font = ln.sidebar_text['font'] 

344 test_font = 'TkTextFont' 

345 self.assertNotEqual(orig_font, test_font) 

346 

347 # Ensure line numbers aren't shown. 

348 ln.hide_sidebar() 

349 

350 self.font_override = test_font 

351 # Nothing breaks when line numbers aren't shown. 

352 ln.update_font() 

353 

354 # Activate line numbers, previous font change is immediately effective. 

355 ln.show_sidebar() 

356 self.assertEqual(ln.sidebar_text['font'], test_font) 

357 

358 # Call the font update with line numbers shown, change is picked up. 

359 self.font_override = orig_font 

360 ln.update_font() 

361 self.assertEqual(ln.sidebar_text['font'], orig_font) 

362 

363 def test_highlight_colors(self): 

364 ln = self.linenumber 

365 

366 orig_colors = dict(self.highlight_cfg) 

367 test_colors = {'background': '#222222', 'foreground': '#ffff00'} 

368 

369 def assert_colors_are_equal(colors): 

370 self.assertEqual(ln.sidebar_text['background'], colors['background']) 

371 self.assertEqual(ln.sidebar_text['foreground'], colors['foreground']) 

372 

373 # Ensure line numbers aren't shown. 

374 ln.hide_sidebar() 

375 

376 self.highlight_cfg = test_colors 

377 # Nothing breaks with inactive line numbers. 

378 ln.update_colors() 

379 

380 # Show line numbers, previous colors change is immediately effective. 

381 ln.show_sidebar() 

382 assert_colors_are_equal(test_colors) 

383 

384 # Call colors update with no change to the configured colors. 

385 ln.update_colors() 

386 assert_colors_are_equal(test_colors) 

387 

388 # Call the colors update with line numbers shown, change is picked up. 

389 self.highlight_cfg = orig_colors 

390 ln.update_colors() 

391 assert_colors_are_equal(orig_colors) 

392 

393 

394class ShellSidebarTest(unittest.TestCase): 

395 root: tk.Tk = None 

396 shell: PyShell = None 

397 

398 @classmethod 

399 def setUpClass(cls): 

400 requires('gui') 

401 

402 cls.root = root = tk.Tk() 

403 root.withdraw() 

404 

405 fix_scaling(root) 

406 fixwordbreaks(root) 

407 fix_x11_paste(root) 

408 

409 cls.flist = flist = PyShellFileList(root) 

410 # See #43981 about macosx.setupApp(root, flist) causing failure. 

411 root.update_idletasks() 

412 

413 cls.init_shell() 

414 

415 @classmethod 

416 def tearDownClass(cls): 

417 if cls.shell is not None: 

418 cls.shell.executing = False 

419 cls.shell.close() 

420 cls.shell = None 

421 cls.flist = None 

422 cls.root.update_idletasks() 

423 cls.root.destroy() 

424 cls.root = None 

425 

426 @classmethod 

427 def init_shell(cls): 

428 cls.shell = cls.flist.open_shell() 

429 cls.shell.pollinterval = 10 

430 cls.root.update() 

431 cls.n_preface_lines = get_lineno(cls.shell.text, 'end-1c') - 1 

432 

433 @classmethod 

434 def reset_shell(cls): 

435 cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c') 

436 cls.shell.shell_sidebar.update_sidebar() 

437 cls.root.update() 

438 

439 def setUp(self): 

440 # In some test environments, e.g. Azure Pipelines (as of 

441 # Apr. 2021), sys.stdout is changed between tests. However, 

442 # PyShell relies on overriding sys.stdout when run without a 

443 # sub-process (as done here; see setUpClass). 

444 self._saved_stdout = None 

445 if sys.stdout != self.shell.stdout: 

446 self._saved_stdout = sys.stdout 

447 sys.stdout = self.shell.stdout 

448 

449 self.reset_shell() 

450 

451 def tearDown(self): 

452 if self._saved_stdout is not None: 

453 sys.stdout = self._saved_stdout 

454 

455 def get_sidebar_lines(self): 

456 canvas = self.shell.shell_sidebar.canvas 

457 texts = list(canvas.find(tk.ALL)) 

458 texts_by_y_coords = { 

459 canvas.bbox(text)[1]: canvas.itemcget(text, 'text') 

460 for text in texts 

461 } 

462 line_y_coords = self.get_shell_line_y_coords() 

463 return [texts_by_y_coords.get(y, None) for y in line_y_coords] 

464 

465 def assert_sidebar_lines_end_with(self, expected_lines): 

466 self.shell.shell_sidebar.update_sidebar() 

467 self.assertEqual( 

468 self.get_sidebar_lines()[-len(expected_lines):], 

469 expected_lines, 

470 ) 

471 

472 def get_shell_line_y_coords(self): 

473 text = self.shell.text 

474 y_coords = [] 

475 index = text.index("@0,0") 

476 if index.split('.', 1)[1] != '0': 

477 index = text.index(f"{index} +1line linestart") 

478 while (lineinfo := text.dlineinfo(index)) is not None: 

479 y_coords.append(lineinfo[1]) 

480 index = text.index(f"{index} +1line") 

481 return y_coords 

482 

483 def get_sidebar_line_y_coords(self): 

484 canvas = self.shell.shell_sidebar.canvas 

485 texts = list(canvas.find(tk.ALL)) 

486 texts.sort(key=lambda text: canvas.bbox(text)[1]) 

487 return [canvas.bbox(text)[1] for text in texts] 

488 

489 def assert_sidebar_lines_synced(self): 

490 self.assertLessEqual( 

491 set(self.get_sidebar_line_y_coords()), 

492 set(self.get_shell_line_y_coords()), 

493 ) 

494 

495 def do_input(self, input): 

496 shell = self.shell 

497 text = shell.text 

498 for line_index, line in enumerate(input.split('\n')): 

499 if line_index > 0: 

500 text.event_generate('<<newline-and-indent>>') 

501 text.insert('insert', line, 'stdin') 

502 

503 def test_initial_state(self): 

504 sidebar_lines = self.get_sidebar_lines() 

505 self.assertEqual( 

506 sidebar_lines, 

507 [None] * (len(sidebar_lines) - 1) + ['>>>'], 

508 ) 

509 self.assert_sidebar_lines_synced() 

510 

511 @run_in_tk_mainloop() 

512 def test_single_empty_input(self): 

513 self.do_input('\n') 

514 yield 

515 self.assert_sidebar_lines_end_with(['>>>', '>>>']) 

516 

517 @run_in_tk_mainloop() 

518 def test_single_line_statement(self): 

519 self.do_input('1\n') 

520 yield 

521 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 

522 

523 @run_in_tk_mainloop() 

524 def test_multi_line_statement(self): 

525 # Block statements are not indented because IDLE auto-indents. 

526 self.do_input(dedent('''\ 

527 if True: 

528 print(1) 

529 

530 ''')) 

531 yield 

532 self.assert_sidebar_lines_end_with([ 

533 '>>>', 

534 '...', 

535 '...', 

536 '...', 

537 None, 

538 '>>>', 

539 ]) 

540 

541 @run_in_tk_mainloop() 

542 def test_single_long_line_wraps(self): 

543 self.do_input('1' * 200 + '\n') 

544 yield 

545 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 

546 self.assert_sidebar_lines_synced() 

547 

548 @run_in_tk_mainloop() 

549 def test_squeeze_multi_line_output(self): 

550 shell = self.shell 

551 text = shell.text 

552 

553 self.do_input('print("a\\nb\\nc")\n') 

554 yield 

555 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) 

556 

557 text.mark_set('insert', f'insert -1line linestart') 

558 text.event_generate('<<squeeze-current-text>>') 

559 yield 

560 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 

561 self.assert_sidebar_lines_synced() 

562 

563 shell.squeezer.expandingbuttons[0].expand() 

564 yield 

565 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) 

566 self.assert_sidebar_lines_synced() 

567 

568 @run_in_tk_mainloop() 

569 def test_interrupt_recall_undo_redo(self): 

570 text = self.shell.text 

571 # Block statements are not indented because IDLE auto-indents. 

572 initial_sidebar_lines = self.get_sidebar_lines() 

573 

574 self.do_input(dedent('''\ 

575 if True: 

576 print(1) 

577 ''')) 

578 yield 

579 self.assert_sidebar_lines_end_with(['>>>', '...', '...']) 

580 with_block_sidebar_lines = self.get_sidebar_lines() 

581 self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) 

582 

583 # Control-C 

584 text.event_generate('<<interrupt-execution>>') 

585 yield 

586 self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>']) 

587 

588 # Recall previous via history 

589 text.event_generate('<<history-previous>>') 

590 text.event_generate('<<interrupt-execution>>') 

591 yield 

592 self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>']) 

593 

594 # Recall previous via recall 

595 text.mark_set('insert', text.index('insert -2l')) 

596 text.event_generate('<<newline-and-indent>>') 

597 yield 

598 

599 text.event_generate('<<undo>>') 

600 yield 

601 self.assert_sidebar_lines_end_with(['>>>']) 

602 

603 text.event_generate('<<redo>>') 

604 yield 

605 self.assert_sidebar_lines_end_with(['>>>', '...']) 

606 

607 text.event_generate('<<newline-and-indent>>') 

608 text.event_generate('<<newline-and-indent>>') 

609 yield 

610 self.assert_sidebar_lines_end_with( 

611 ['>>>', '...', '...', '...', None, '>>>'] 

612 ) 

613 

614 @run_in_tk_mainloop() 

615 def test_very_long_wrapped_line(self): 

616 with support.adjust_int_max_str_digits(11_111), \ 

617 swap_attr(self.shell, 'squeezer', None): 

618 self.do_input('x = ' + '1'*10_000 + '\n') 

619 yield 

620 self.assertEqual(self.get_sidebar_lines(), ['>>>']) 

621 

622 def test_font(self): 

623 sidebar = self.shell.shell_sidebar 

624 

625 test_font = 'TkTextFont' 

626 

627 def mock_idleconf_GetFont(root, configType, section): 

628 return test_font 

629 GetFont_patcher = unittest.mock.patch.object( 

630 idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) 

631 GetFont_patcher.start() 

632 def cleanup(): 

633 GetFont_patcher.stop() 

634 sidebar.update_font() 

635 self.addCleanup(cleanup) 

636 

637 def get_sidebar_font(): 

638 canvas = sidebar.canvas 

639 texts = list(canvas.find(tk.ALL)) 

640 fonts = {canvas.itemcget(text, 'font') for text in texts} 

641 self.assertEqual(len(fonts), 1) 

642 return next(iter(fonts)) 

643 

644 self.assertNotEqual(get_sidebar_font(), test_font) 

645 sidebar.update_font() 

646 self.assertEqual(get_sidebar_font(), test_font) 

647 

648 def test_highlight_colors(self): 

649 sidebar = self.shell.shell_sidebar 

650 

651 test_colors = {"background": '#abcdef', "foreground": '#123456'} 

652 

653 orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight 

654 def mock_idleconf_GetHighlight(theme, element): 

655 if element in ['linenumber', 'console']: 

656 return test_colors 

657 return orig_idleConf_GetHighlight(theme, element) 

658 GetHighlight_patcher = unittest.mock.patch.object( 

659 idlelib.sidebar.idleConf, 'GetHighlight', 

660 mock_idleconf_GetHighlight) 

661 GetHighlight_patcher.start() 

662 def cleanup(): 

663 GetHighlight_patcher.stop() 

664 sidebar.update_colors() 

665 self.addCleanup(cleanup) 

666 

667 def get_sidebar_colors(): 

668 canvas = sidebar.canvas 

669 texts = list(canvas.find(tk.ALL)) 

670 fgs = {canvas.itemcget(text, 'fill') for text in texts} 

671 self.assertEqual(len(fgs), 1) 

672 fg = next(iter(fgs)) 

673 bg = canvas.cget('background') 

674 return {"background": bg, "foreground": fg} 

675 

676 self.assertNotEqual(get_sidebar_colors(), test_colors) 

677 sidebar.update_colors() 

678 self.assertEqual(get_sidebar_colors(), test_colors) 

679 

680 @run_in_tk_mainloop() 

681 def test_mousewheel(self): 

682 sidebar = self.shell.shell_sidebar 

683 text = self.shell.text 

684 

685 # Enter a 100-line string to scroll the shell screen down. 

686 self.do_input('x = """' + '\n'*100 + '"""\n') 

687 yield 

688 self.assertGreater(get_lineno(text, '@0,0'), 1) 

689 

690 last_lineno = get_end_linenumber(text) 

691 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 

692 

693 # Scroll up using the <MouseWheel> event. 

694 # The meaning delta is platform-dependant. 

695 delta = -1 if sys.platform == 'darwin' else 120 

696 sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta) 

697 yield 

698 self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 

699 

700 # Scroll back down using the <Button-5> event. 

701 sidebar.canvas.event_generate('<Button-5>', x=0, y=0) 

702 yield 

703 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 

704 

705 @run_in_tk_mainloop() 

706 def test_copy(self): 

707 sidebar = self.shell.shell_sidebar 

708 text = self.shell.text 

709 

710 first_line = get_end_linenumber(text) 

711 

712 self.do_input(dedent('''\ 

713 if True: 

714 print(1) 

715 

716 ''')) 

717 yield 

718 

719 text.tag_add('sel', f'{first_line}.0', 'end-1c') 

720 selected_text = text.get('sel.first', 'sel.last') 

721 self.assertTrue(selected_text.startswith('if True:\n')) 

722 self.assertIn('\n1\n', selected_text) 

723 

724 text.event_generate('<<copy>>') 

725 self.addCleanup(text.clipboard_clear) 

726 

727 copied_text = text.clipboard_get() 

728 self.assertEqual(copied_text, selected_text) 

729 

730 @run_in_tk_mainloop() 

731 def test_copy_with_prompts(self): 

732 sidebar = self.shell.shell_sidebar 

733 text = self.shell.text 

734 

735 first_line = get_end_linenumber(text) 

736 self.do_input(dedent('''\ 

737 if True: 

738 print(1) 

739 

740 ''')) 

741 yield 

742 

743 text.tag_add('sel', f'{first_line}.3', 'end-1c') 

744 selected_text = text.get('sel.first', 'sel.last') 

745 self.assertTrue(selected_text.startswith('True:\n')) 

746 

747 selected_lines_text = text.get('sel.first linestart', 'sel.last') 

748 selected_lines = selected_lines_text.split('\n') 

749 selected_lines.pop() # Final '' is a split artifact, not a line. 

750 # Expect a block of input and a single output line. 

751 expected_prompts = \ 

752 ['>>>'] + ['...'] * (len(selected_lines) - 2) + [None] 

753 selected_text_with_prompts = '\n'.join( 

754 line if prompt is None else prompt + ' ' + line 

755 for prompt, line in zip(expected_prompts, 

756 selected_lines, 

757 strict=True) 

758 ) + '\n' 

759 

760 text.event_generate('<<copy-with-prompts>>') 

761 self.addCleanup(text.clipboard_clear) 

762 

763 copied_text = text.clipboard_get() 

764 self.assertEqual(copied_text, selected_text_with_prompts) 

765 

766 

767if __name__ == '__main__': 767 ↛ 768line 767 didn't jump to line 768, because the condition on line 767 was never true

768 unittest.main(verbosity=2)