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
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1"Test codecontext, coverage 100%"
3from idlelib import codecontext
4import unittest
5import unittest.mock
6from test.support import requires
7from tkinter import NSEW, Tk, Frame, Text, TclError
9from unittest import mock
10import re
11from idlelib import config
14usercfg = codecontext.idleConf.userCfg
15testcfg = {
16 'main': config.IdleUserConfParser(''),
17 'highlight': config.IdleUserConfParser(''),
18 'keys': config.IdleUserConfParser(''),
19 'extensions': config.IdleUserConfParser(''),
20}
21code_sample = """\
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"""
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 = ''
46 def getlineno(self, index):
47 return int(float(self.text.index(index)))
49 def update_menu_label(self, **kwargs):
50 self.label = kwargs['label']
53class CodeContextTest(unittest.TestCase):
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
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
78 def setUp(self):
79 self.text.yview(0)
80 self.text['font'] = 'TkFixedFont'
81 self.cc = codecontext.CodeContext(self.editor)
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)
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)
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
110 def test_init(self):
111 eq = self.assertEqual
112 ed = self.editor
113 cc = self.cc
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)
123 def test_del(self):
124 self.cc.__del__()
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))
133 def test_reload(self):
134 codecontext.CodeContext.reload()
135 self.assertEqual(self.cc.context_depth, 15)
137 def test_toggle_code_context_event(self):
138 eq = self.assertEqual
139 cc = self.cc
140 toggle = cc.toggle_code_context_event
142 # Make sure code context is off.
143 if cc.context:
144 toggle()
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')
156 # Toggle off.
157 toggle()
158 self.assertIsNone(cc.context)
159 eq(cc.editwin.label, 'Show Code Context')
160 self.assertIsNone(self.cc.t1)
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)
168 # Toggle off and on again.
169 toggle()
170 toggle()
171 eq(cc.context.get('1.0', 'end-1c'), line11_context)
173 def test_get_context(self):
174 eq = self.assertEqual
175 gc = self.cc.get_context
177 # stopline must be greater than 0.
178 with self.assertRaises(AssertionError):
179 gc(1, stopline=0)
181 eq(gc(3), ([(2, 0, 'class C1:', 'class')], 0))
183 # Don't return comment.
184 eq(gc(4), ([(2, 0, 'class C1:', 'class')], 0))
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))
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))
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))
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))
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))
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()
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)
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'), '')
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:')
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:')
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):')
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:')
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:')
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):')
302 def test_jumptoline(self):
303 eq = self.assertEqual
304 cc = self.cc
305 jump = cc.jumptoline
307 if not cc.context:
308 cc.toggle_code_context_event()
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)
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)
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)
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)
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()
351 # Activate code context.
352 self.cc.toggle_code_context_event()
353 self.cc.timer_event()
354 mock_update.assert_called()
356 def test_font(self):
357 eq = self.assertEqual
358 cc = self.cc
360 orig_font = cc.text['font']
361 test_font = 'TkTextFont'
362 self.assertNotEqual(orig_font, test_font)
364 # Ensure code context is not active.
365 if cc.context is not None:
366 cc.toggle_code_context_event()
368 self.font_override = test_font
369 # Nothing breaks or changes with inactive code context.
370 cc.update_font()
372 # Activate code context, previous font change is immediately effective.
373 cc.toggle_code_context_event()
374 eq(cc.context['font'], test_font)
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)
381 def test_highlight_colors(self):
382 eq = self.assertEqual
383 cc = self.cc
385 orig_colors = dict(self.highlight_cfg)
386 test_colors = {'background': '#222222', 'foreground': '#ffff00'}
388 def assert_colors_are_equal(colors):
389 eq(cc.context['background'], colors['background'])
390 eq(cc.context['foreground'], colors['foreground'])
392 # Ensure code context is not active.
393 if cc.context:
394 cc.toggle_code_context_event()
396 self.highlight_cfg = test_colors
397 # Nothing breaks with inactive code context.
398 cc.update_highlight_colors()
400 # Activate code context, previous colors change is immediately effective.
401 cc.toggle_code_context_event()
402 assert_colors_are_equal(test_colors)
404 # Call colors update with no change to the configured colors.
405 cc.update_highlight_colors()
406 assert_colors_are_equal(test_colors)
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)
414class HelperFunctionText(unittest.TestCase):
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
430 # Send the pattern in the call.
431 self.assertEqual(get(' (continuation)', 1c
432 c=re.compile(r'^(\s*)([^\s]*)')),
433 (' ', '(continuation)'))
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
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
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)