• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
418        test_lines = (
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:
428            self.assertEqual(get(line), expected_output)
429
430        # Send the pattern in the call.
431        self.assertEqual(get('    (continuation)',
432                             c=re.compile(r'^(\s*)([^\s]*)')),
433                         ('    ', '(continuation)'))
434
435    def test_get_line_info(self):
436        eq = self.assertEqual
437        gli = codecontext.get_line_info
438        lines = code_sample.splitlines()
439
440        # Line 1 is not a BLOCKOPENER.
441        eq(gli(lines[0]), (codecontext.INFINITY, '', False))
442        # Line 2 is a BLOCKOPENER without an indent.
443        eq(gli(lines[1]), (0, 'class C1():', 'class'))
444        # Line 3 is not a BLOCKOPENER and does not return the indent level.
445        eq(gli(lines[2]), (codecontext.INFINITY, '    # Class comment.', False))
446        # Line 4 is a BLOCKOPENER and is indented.
447        eq(gli(lines[3]), (4, '    def __init__(self, a, b):', 'def'))
448        # Line 8 is a different BLOCKOPENER and is indented.
449        eq(gli(lines[7]), (8, '        if a > b:', 'if'))
450        # Test tab.
451        eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if'))
452
453
454if __name__ == '__main__':
455    unittest.main(verbosity=2)
456