Coverage for idle_test/test_codecontext.py: 21%

265 statements  

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

1"Test codecontext, coverage 100%" 

2 

3from idlelib import codecontext 

4import unittest 

5import unittest.mock 

6from test.support import requires 

7from tkinter import NSEW, Tk, Frame, Text, TclError 

8 

9from unittest import mock 

10import re 

11from idlelib import config 

12 

13 

14usercfg = codecontext.idleConf.userCfg 

15testcfg = { 

16 'main': config.IdleUserConfParser(''), 

17 'highlight': config.IdleUserConfParser(''), 

18 'keys': config.IdleUserConfParser(''), 

19 'extensions': config.IdleUserConfParser(''), 

20} 

21code_sample = """\ 

22 

23class C1: 

24 # Class comment. 

25 def __init__(self, a, b): 

26 self.a = a 

27 self.b = b 

28 def compare(self): 

29 if a > b: 

30 return a 

31 elif a < b: 

32 return b 

33 else: 

34 return None 

35""" 

36 

37 

38class DummyEditwin: 

39 def __init__(self, root, frame, text): 

40 self.root = root 

41 self.top = root 

42 self.text_frame = frame 

43 self.text = text 

44 self.label = '' 

45 

46 def getlineno(self, index): 

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

48 

49 def update_menu_label(self, **kwargs): 

50 self.label = kwargs['label'] 

51 

52 

53class CodeContextTest(unittest.TestCase): 

54 

55 @classmethod 

56 def setUpClass(cls): 

57 requires('gui') 

58 root = cls.root = Tk() 

59 root.withdraw() 

60 frame = cls.frame = Frame(root) 

61 text = cls.text = Text(frame) 

62 text.insert('1.0', code_sample) 

63 # Need to pack for creation of code context text widget. 

64 frame.pack(side='left', fill='both', expand=1) 

65 text.grid(row=1, column=1, sticky=NSEW) 

66 cls.editor = DummyEditwin(root, frame, text) 

67 codecontext.idleConf.userCfg = testcfg 

68 

69 @classmethod 

70 def tearDownClass(cls): 

71 codecontext.idleConf.userCfg = usercfg 

72 cls.editor.text.delete('1.0', 'end') 

73 del cls.editor, cls.frame, cls.text 

74 cls.root.update_idletasks() 

75 cls.root.destroy() 

76 del cls.root 

77 

78 def setUp(self): 

79 self.text.yview(0) 

80 self.text['font'] = 'TkFixedFont' 

81 self.cc = codecontext.CodeContext(self.editor) 

82 

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

84 "foreground": '#123456'} 

85 orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight 

86 def mock_idleconf_GetHighlight(theme, element): 

87 if element == 'context': 

88 return self.highlight_cfg 

89 return orig_idleConf_GetHighlight(theme, element) 

90 GetHighlight_patcher = unittest.mock.patch.object( 

91 codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) 

92 GetHighlight_patcher.start() 

93 self.addCleanup(GetHighlight_patcher.stop) 

94 

95 self.font_override = 'TkFixedFont' 

96 def mock_idleconf_GetFont(root, configType, section): 

97 return self.font_override 

98 GetFont_patcher = unittest.mock.patch.object( 

99 codecontext.idleConf, 'GetFont', mock_idleconf_GetFont) 

100 GetFont_patcher.start() 

101 self.addCleanup(GetFont_patcher.stop) 

102 

103 def tearDown(self): 

104 if self.cc.context: 

105 self.cc.context.destroy() 

106 # Explicitly call __del__ to remove scheduled scripts. 

107 self.cc.__del__() 

108 del self.cc.context, self.cc 

109 

110 def test_init(self): 

111 eq = self.assertEqual 

112 ed = self.editor 

113 cc = self.cc 

114 

115 eq(cc.editwin, ed) 

116 eq(cc.text, ed.text) 

117 eq(cc.text['font'], ed.text['font']) 

118 self.assertIsNone(cc.context) 

119 eq(cc.info, [(0, -1, '', False)]) 

120 eq(cc.topvisible, 1) 

121 self.assertIsNone(self.cc.t1) 

122 

123 def test_del(self): 

124 self.cc.__del__() 

125 

126 def test_del_with_timer(self): 

127 timer = self.cc.t1 = self.text.after(10000, lambda: None) 

128 self.cc.__del__() 

129 with self.assertRaises(TclError) as cm: 

130 self.root.tk.call('after', 'info', timer) 

131 self.assertIn("doesn't exist", str(cm.exception)) 

132 

133 def test_reload(self): 

134 codecontext.CodeContext.reload() 

135 self.assertEqual(self.cc.context_depth, 15) 

136 

137 def test_toggle_code_context_event(self): 

138 eq = self.assertEqual 

139 cc = self.cc 

140 toggle = cc.toggle_code_context_event 

141 

142 # Make sure code context is off. 

143 if cc.context: 

144 toggle() 

145 

146 # Toggle on. 

147 toggle() 

148 self.assertIsNotNone(cc.context) 

149 eq(cc.context['font'], self.text['font']) 

150 eq(cc.context['fg'], self.highlight_cfg['foreground']) 

151 eq(cc.context['bg'], self.highlight_cfg['background']) 

152 eq(cc.context.get('1.0', 'end-1c'), '') 

153 eq(cc.editwin.label, 'Hide Code Context') 

154 eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer') 

155 

156 # Toggle off. 

157 toggle() 

158 self.assertIsNone(cc.context) 

159 eq(cc.editwin.label, 'Show Code Context') 

160 self.assertIsNone(self.cc.t1) 

161 

162 # Scroll down and toggle back on. 

163 line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0]) 

164 cc.text.yview(11) 

165 toggle() 

166 eq(cc.context.get('1.0', 'end-1c'), line11_context) 

167 

168 # Toggle off and on again. 

169 toggle() 

170 toggle() 

171 eq(cc.context.get('1.0', 'end-1c'), line11_context) 

172 

173 def test_get_context(self): 

174 eq = self.assertEqual 

175 gc = self.cc.get_context 

176 

177 # stopline must be greater than 0. 

178 with self.assertRaises(AssertionError): 

179 gc(1, stopline=0) 

180 

181 eq(gc(3), ([(2, 0, 'class C1:', 'class')], 0)) 

182 

183 # Don't return comment. 

184 eq(gc(4), ([(2, 0, 'class C1:', 'class')], 0)) 

185 

186 # Two indentation levels and no comment. 

187 eq(gc(5), ([(2, 0, 'class C1:', 'class'), 

188 (4, 4, ' def __init__(self, a, b):', 'def')], 0)) 

189 

190 # Only one 'def' is returned, not both at the same indent level. 

191 eq(gc(10), ([(2, 0, 'class C1:', 'class'), 

192 (7, 4, ' def compare(self):', 'def'), 

193 (8, 8, ' if a > b:', 'if')], 0)) 

194 

195 # With 'elif', also show the 'if' even though it's at the same level. 

196 eq(gc(11), ([(2, 0, 'class C1:', 'class'), 

197 (7, 4, ' def compare(self):', 'def'), 

198 (8, 8, ' if a > b:', 'if'), 

199 (10, 8, ' elif a < b:', 'elif')], 0)) 

200 

201 # Set stop_line to not go back to first line in source code. 

202 # Return includes stop_line. 

203 eq(gc(11, stopline=2), ([(2, 0, 'class C1:', 'class'), 

204 (7, 4, ' def compare(self):', 'def'), 

205 (8, 8, ' if a > b:', 'if'), 

206 (10, 8, ' elif a < b:', 'elif')], 0)) 

207 eq(gc(11, stopline=3), ([(7, 4, ' def compare(self):', 'def'), 

208 (8, 8, ' if a > b:', 'if'), 

209 (10, 8, ' elif a < b:', 'elif')], 4)) 

210 eq(gc(11, stopline=8), ([(8, 8, ' if a > b:', 'if'), 

211 (10, 8, ' elif a < b:', 'elif')], 8)) 

212 

213 # Set stop_indent to test indent level to stop at. 

214 eq(gc(11, stopindent=4), ([(7, 4, ' def compare(self):', 'def'), 

215 (8, 8, ' if a > b:', 'if'), 

216 (10, 8, ' elif a < b:', 'elif')], 4)) 

217 # Check that the 'if' is included. 

218 eq(gc(11, stopindent=8), ([(8, 8, ' if a > b:', 'if'), 

219 (10, 8, ' elif a < b:', 'elif')], 8)) 

220 

221 def test_update_code_context(self): 

222 eq = self.assertEqual 

223 cc = self.cc 

224 # Ensure code context is active. 

225 if not cc.context: 

226 cc.toggle_code_context_event() 

227 

228 # Invoke update_code_context without scrolling - nothing happens. 

229 self.assertIsNone(cc.update_code_context()) 

230 eq(cc.info, [(0, -1, '', False)]) 

231 eq(cc.topvisible, 1) 

232 

233 # Scroll down to line 1. 

234 cc.text.yview(1) 

235 cc.update_code_context() 

236 eq(cc.info, [(0, -1, '', False)]) 

237 eq(cc.topvisible, 2) 

238 eq(cc.context.get('1.0', 'end-1c'), '') 

239 

240 # Scroll down to line 2. 

241 cc.text.yview(2) 

242 cc.update_code_context() 

243 eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')]) 

244 eq(cc.topvisible, 3) 

245 eq(cc.context.get('1.0', 'end-1c'), 'class C1:') 

246 

247 # Scroll down to line 3. Since it's a comment, nothing changes. 

248 cc.text.yview(3) 

249 cc.update_code_context() 

250 eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')]) 

251 eq(cc.topvisible, 4) 

252 eq(cc.context.get('1.0', 'end-1c'), 'class C1:') 

253 

254 # Scroll down to line 4. 

255 cc.text.yview(4) 

256 cc.update_code_context() 

257 eq(cc.info, [(0, -1, '', False), 

258 (2, 0, 'class C1:', 'class'), 

259 (4, 4, ' def __init__(self, a, b):', 'def')]) 

260 eq(cc.topvisible, 5) 

261 eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n' 

262 ' def __init__(self, a, b):') 

263 

264 # Scroll down to line 11. Last 'def' is removed. 

265 cc.text.yview(11) 

266 cc.update_code_context() 

267 eq(cc.info, [(0, -1, '', False), 

268 (2, 0, 'class C1:', 'class'), 

269 (7, 4, ' def compare(self):', 'def'), 

270 (8, 8, ' if a > b:', 'if'), 

271 (10, 8, ' elif a < b:', 'elif')]) 

272 eq(cc.topvisible, 12) 

273 eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n' 

274 ' def compare(self):\n' 

275 ' if a > b:\n' 

276 ' elif a < b:') 

277 

278 # No scroll. No update, even though context_depth changed. 

279 cc.update_code_context() 

280 cc.context_depth = 1 

281 eq(cc.info, [(0, -1, '', False), 

282 (2, 0, 'class C1:', 'class'), 

283 (7, 4, ' def compare(self):', 'def'), 

284 (8, 8, ' if a > b:', 'if'), 

285 (10, 8, ' elif a < b:', 'elif')]) 

286 eq(cc.topvisible, 12) 

287 eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n' 

288 ' def compare(self):\n' 

289 ' if a > b:\n' 

290 ' elif a < b:') 

291 

292 # Scroll up. 

293 cc.text.yview(5) 

294 cc.update_code_context() 

295 eq(cc.info, [(0, -1, '', False), 

296 (2, 0, 'class C1:', 'class'), 

297 (4, 4, ' def __init__(self, a, b):', 'def')]) 

298 eq(cc.topvisible, 6) 

299 # context_depth is 1. 

300 eq(cc.context.get('1.0', 'end-1c'), ' def __init__(self, a, b):') 

301 

302 def test_jumptoline(self): 

303 eq = self.assertEqual 

304 cc = self.cc 

305 jump = cc.jumptoline 

306 

307 if not cc.context: 

308 cc.toggle_code_context_event() 

309 

310 # Empty context. 

311 cc.text.yview('2.0') 

312 cc.update_code_context() 

313 eq(cc.topvisible, 2) 

314 cc.context.mark_set('insert', '1.5') 

315 jump() 

316 eq(cc.topvisible, 1) 

317 

318 # 4 lines of context showing. 

319 cc.text.yview('12.0') 

320 cc.update_code_context() 

321 eq(cc.topvisible, 12) 

322 cc.context.mark_set('insert', '3.0') 

323 jump() 

324 eq(cc.topvisible, 8) 

325 

326 # More context lines than limit. 

327 cc.context_depth = 2 

328 cc.text.yview('12.0') 

329 cc.update_code_context() 

330 eq(cc.topvisible, 12) 

331 cc.context.mark_set('insert', '1.0') 

332 jump() 

333 eq(cc.topvisible, 8) 

334 

335 # Context selection stops jump. 

336 cc.text.yview('5.0') 

337 cc.update_code_context() 

338 cc.context.tag_add('sel', '1.0', '2.0') 

339 cc.context.mark_set('insert', '1.0') 

340 jump() # Without selection, to line 2. 

341 eq(cc.topvisible, 5) 

342 

343 @mock.patch.object(codecontext.CodeContext, 'update_code_context') 

344 def test_timer_event(self, mock_update): 

345 # Ensure code context is not active. 

346 if self.cc.context: 

347 self.cc.toggle_code_context_event() 

348 self.cc.timer_event() 

349 mock_update.assert_not_called() 

350 

351 # Activate code context. 

352 self.cc.toggle_code_context_event() 

353 self.cc.timer_event() 

354 mock_update.assert_called() 

355 

356 def test_font(self): 

357 eq = self.assertEqual 

358 cc = self.cc 

359 

360 orig_font = cc.text['font'] 

361 test_font = 'TkTextFont' 

362 self.assertNotEqual(orig_font, test_font) 

363 

364 # Ensure code context is not active. 

365 if cc.context is not None: 

366 cc.toggle_code_context_event() 

367 

368 self.font_override = test_font 

369 # Nothing breaks or changes with inactive code context. 

370 cc.update_font() 

371 

372 # Activate code context, previous font change is immediately effective. 

373 cc.toggle_code_context_event() 

374 eq(cc.context['font'], test_font) 

375 

376 # Call the font update, change is picked up. 

377 self.font_override = orig_font 

378 cc.update_font() 

379 eq(cc.context['font'], orig_font) 

380 

381 def test_highlight_colors(self): 

382 eq = self.assertEqual 

383 cc = self.cc 

384 

385 orig_colors = dict(self.highlight_cfg) 

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

387 

388 def assert_colors_are_equal(colors): 

389 eq(cc.context['background'], colors['background']) 

390 eq(cc.context['foreground'], colors['foreground']) 

391 

392 # Ensure code context is not active. 

393 if cc.context: 

394 cc.toggle_code_context_event() 

395 

396 self.highlight_cfg = test_colors 

397 # Nothing breaks with inactive code context. 

398 cc.update_highlight_colors() 

399 

400 # Activate code context, previous colors change is immediately effective. 

401 cc.toggle_code_context_event() 

402 assert_colors_are_equal(test_colors) 

403 

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

405 cc.update_highlight_colors() 

406 assert_colors_are_equal(test_colors) 

407 

408 # Call the colors update with code context active, change is picked up. 

409 self.highlight_cfg = orig_colors 

410 cc.update_highlight_colors() 

411 assert_colors_are_equal(orig_colors) 

412 

413 

414class HelperFunctionText(unittest.TestCase): 

415 

416 def test_get_spaces_firstword(self): 

417 get = codecontext.get_spaces_firstword 1c

418 test_lines = ( 1c

419 (' first word', (' ', 'first')), 

420 ('\tfirst word', ('\t', 'first')), 

421 (' \u19D4\u19D2: ', (' ', '\u19D4\u19D2')), 

422 ('no spaces', ('', 'no')), 

423 ('', ('', '')), 

424 ('# TEST COMMENT', ('', '')), 

425 (' (continuation)', (' ', '')) 

426 ) 

427 for line, expected_output in test_lines: 1c

428 self.assertEqual(get(line), expected_output) 1c

429 

430 # Send the pattern in the call. 

431 self.assertEqual(get(' (continuation)', 1c

432 c=re.compile(r'^(\s*)([^\s]*)')), 

433 (' ', '(continuation)')) 

434 

435 def test_get_line_info(self): 

436 eq = self.assertEqual 1b

437 gli = codecontext.get_line_info 1b

438 lines = code_sample.splitlines() 1b

439 

440 # Line 1 is not a BLOCKOPENER. 

441 eq(gli(lines[0]), (codecontext.INFINITY, '', False)) 1b

442 # Line 2 is a BLOCKOPENER without an indent. 

443 eq(gli(lines[1]), (0, 'class C1:', 'class')) 1b

444 # Line 3 is not a BLOCKOPENER and does not return the indent level. 

445 eq(gli(lines[2]), (codecontext.INFINITY, ' # Class comment.', False)) 1b

446 # Line 4 is a BLOCKOPENER and is indented. 

447 eq(gli(lines[3]), (4, ' def __init__(self, a, b):', 'def')) 1b

448 # Line 8 is a different BLOCKOPENER and is indented. 

449 eq(gli(lines[7]), (8, ' if a > b:', 'if')) 1b

450 # Test tab. 

451 eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if')) 1b

452 

453 

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

455 unittest.main(verbosity=2)