• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"Test colorizer, coverage 93%."
2
3from idlelib import colorizer
4from test.support import requires
5import unittest
6from unittest import mock
7
8from functools import partial
9from tkinter import Tk, Text
10from idlelib import config
11from idlelib.percolator import Percolator
12
13
14usercfg = colorizer.idleConf.userCfg
15testcfg = {
16    'main': config.IdleUserConfParser(''),
17    'highlight': config.IdleUserConfParser(''),
18    'keys': config.IdleUserConfParser(''),
19    'extensions': config.IdleUserConfParser(''),
20}
21
22source = (
23    "if True: int ('1') # keyword, builtin, string, comment\n"
24    "elif False: print(0)  # 'string' in comment\n"
25    "else: float(None)  # if in comment\n"
26    "if iF + If + IF: 'keyword matching must respect case'\n"
27    "if'': x or''  # valid string-keyword no-space combinations\n"
28    "async def f(): await g()\n"
29    "'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
30    )
31
32
33def setUpModule():
34    colorizer.idleConf.userCfg = testcfg
35
36
37def tearDownModule():
38    colorizer.idleConf.userCfg = usercfg
39
40
41class FunctionTest(unittest.TestCase):
42
43    def test_any(self):
44        self.assertEqual(colorizer.any('test', ('a', 'b', 'cd')),
45                         '(?P<test>a|b|cd)')
46
47    def test_make_pat(self):
48        # Tested in more detail by testing prog.
49        self.assertTrue(colorizer.make_pat())
50
51    def test_prog(self):
52        prog = colorizer.prog
53        eq = self.assertEqual
54        line = 'def f():\n    print("hello")\n'
55        m = prog.search(line)
56        eq(m.groupdict()['KEYWORD'], 'def')
57        m = prog.search(line, m.end())
58        eq(m.groupdict()['SYNC'], '\n')
59        m = prog.search(line, m.end())
60        eq(m.groupdict()['BUILTIN'], 'print')
61        m = prog.search(line, m.end())
62        eq(m.groupdict()['STRING'], '"hello"')
63        m = prog.search(line, m.end())
64        eq(m.groupdict()['SYNC'], '\n')
65
66    def test_idprog(self):
67        idprog = colorizer.idprog
68        m = idprog.match('nospace')
69        self.assertIsNone(m)
70        m = idprog.match(' space')
71        self.assertEqual(m.group(0), ' space')
72
73
74class ColorConfigTest(unittest.TestCase):
75
76    @classmethod
77    def setUpClass(cls):
78        requires('gui')
79        root = cls.root = Tk()
80        root.withdraw()
81        cls.text = Text(root)
82
83    @classmethod
84    def tearDownClass(cls):
85        del cls.text
86        cls.root.update_idletasks()
87        cls.root.destroy()
88        del cls.root
89
90    def test_color_config(self):
91        text = self.text
92        eq = self.assertEqual
93        colorizer.color_config(text)
94        # Uses IDLE Classic theme as default.
95        eq(text['background'], '#ffffff')
96        eq(text['foreground'], '#000000')
97        eq(text['selectbackground'], 'gray')
98        eq(text['selectforeground'], '#000000')
99        eq(text['insertbackground'], 'black')
100        eq(text['inactiveselectbackground'], 'gray')
101
102
103class ColorDelegatorInstantiationTest(unittest.TestCase):
104
105    @classmethod
106    def setUpClass(cls):
107        requires('gui')
108        root = cls.root = Tk()
109        root.withdraw()
110        text = cls.text = Text(root)
111
112    @classmethod
113    def tearDownClass(cls):
114        del cls.text
115        cls.root.update_idletasks()
116        cls.root.destroy()
117        del cls.root
118
119    def setUp(self):
120        self.color = colorizer.ColorDelegator()
121
122    def tearDown(self):
123        self.color.close()
124        self.text.delete('1.0', 'end')
125        self.color.resetcache()
126        del self.color
127
128    def test_init(self):
129        color = self.color
130        self.assertIsInstance(color, colorizer.ColorDelegator)
131
132    def test_init_state(self):
133        # init_state() is called during the instantiation of
134        # ColorDelegator in setUp().
135        color = self.color
136        self.assertIsNone(color.after_id)
137        self.assertTrue(color.allow_colorizing)
138        self.assertFalse(color.colorizing)
139        self.assertFalse(color.stop_colorizing)
140
141
142class ColorDelegatorTest(unittest.TestCase):
143
144    @classmethod
145    def setUpClass(cls):
146        requires('gui')
147        root = cls.root = Tk()
148        root.withdraw()
149        text = cls.text = Text(root)
150        cls.percolator = Percolator(text)
151        # Delegator stack = [Delegator(text)]
152
153    @classmethod
154    def tearDownClass(cls):
155        cls.percolator.redir.close()
156        del cls.percolator, cls.text
157        cls.root.update_idletasks()
158        cls.root.destroy()
159        del cls.root
160
161    def setUp(self):
162        self.color = colorizer.ColorDelegator()
163        self.percolator.insertfilter(self.color)
164        # Calls color.setdelegate(Delegator(text)).
165
166    def tearDown(self):
167        self.color.close()
168        self.percolator.removefilter(self.color)
169        self.text.delete('1.0', 'end')
170        self.color.resetcache()
171        del self.color
172
173    def test_setdelegate(self):
174        # Called in setUp when filter is attached to percolator.
175        color = self.color
176        self.assertIsInstance(color.delegate, colorizer.Delegator)
177        # It is too late to mock notify_range, so test side effect.
178        self.assertEqual(self.root.tk.call(
179            'after', 'info', color.after_id)[1], 'timer')
180
181    def test_LoadTagDefs(self):
182        highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
183        for tag, colors in self.color.tagdefs.items():
184            with self.subTest(tag=tag):
185                self.assertIn('background', colors)
186                self.assertIn('foreground', colors)
187                if tag not in ('SYNC', 'TODO'):
188                    self.assertEqual(colors, highlight(element=tag.lower()))
189
190    def test_config_colors(self):
191        text = self.text
192        highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
193        for tag in self.color.tagdefs:
194            for plane in ('background', 'foreground'):
195                with self.subTest(tag=tag, plane=plane):
196                    if tag in ('SYNC', 'TODO'):
197                        self.assertEqual(text.tag_cget(tag, plane), '')
198                    else:
199                        self.assertEqual(text.tag_cget(tag, plane),
200                                         highlight(element=tag.lower())[plane])
201        # 'sel' is marked as the highest priority.
202        self.assertEqual(text.tag_names()[-1], 'sel')
203
204    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
205    def test_insert(self, mock_notify):
206        text = self.text
207        # Initial text.
208        text.insert('insert', 'foo')
209        self.assertEqual(text.get('1.0', 'end'), 'foo\n')
210        mock_notify.assert_called_with('1.0', '1.0+3c')
211        # Additional text.
212        text.insert('insert', 'barbaz')
213        self.assertEqual(text.get('1.0', 'end'), 'foobarbaz\n')
214        mock_notify.assert_called_with('1.3', '1.3+6c')
215
216    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
217    def test_delete(self, mock_notify):
218        text = self.text
219        # Initialize text.
220        text.insert('insert', 'abcdefghi')
221        self.assertEqual(text.get('1.0', 'end'), 'abcdefghi\n')
222        # Delete single character.
223        text.delete('1.7')
224        self.assertEqual(text.get('1.0', 'end'), 'abcdefgi\n')
225        mock_notify.assert_called_with('1.7')
226        # Delete multiple characters.
227        text.delete('1.3', '1.6')
228        self.assertEqual(text.get('1.0', 'end'), 'abcgi\n')
229        mock_notify.assert_called_with('1.3')
230
231    def test_notify_range(self):
232        text = self.text
233        color = self.color
234        eq = self.assertEqual
235
236        # Colorizing already scheduled.
237        save_id = color.after_id
238        eq(self.root.tk.call('after', 'info', save_id)[1], 'timer')
239        self.assertFalse(color.colorizing)
240        self.assertFalse(color.stop_colorizing)
241        self.assertTrue(color.allow_colorizing)
242
243        # Coloring scheduled and colorizing in progress.
244        color.colorizing = True
245        color.notify_range('1.0', 'end')
246        self.assertFalse(color.stop_colorizing)
247        eq(color.after_id, save_id)
248
249        # No colorizing scheduled and colorizing in progress.
250        text.after_cancel(save_id)
251        color.after_id = None
252        color.notify_range('1.0', '1.0+3c')
253        self.assertTrue(color.stop_colorizing)
254        self.assertIsNotNone(color.after_id)
255        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
256        # New event scheduled.
257        self.assertNotEqual(color.after_id, save_id)
258
259        # No colorizing scheduled and colorizing off.
260        text.after_cancel(color.after_id)
261        color.after_id = None
262        color.allow_colorizing = False
263        color.notify_range('1.4', '1.4+10c')
264        # Nothing scheduled when colorizing is off.
265        self.assertIsNone(color.after_id)
266
267    def test_toggle_colorize_event(self):
268        color = self.color
269        eq = self.assertEqual
270
271        # Starts with colorizing allowed and scheduled.
272        self.assertFalse(color.colorizing)
273        self.assertFalse(color.stop_colorizing)
274        self.assertTrue(color.allow_colorizing)
275        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
276
277        # Toggle colorizing off.
278        color.toggle_colorize_event()
279        self.assertIsNone(color.after_id)
280        self.assertFalse(color.colorizing)
281        self.assertFalse(color.stop_colorizing)
282        self.assertFalse(color.allow_colorizing)
283
284        # Toggle on while colorizing in progress (doesn't add timer).
285        color.colorizing = True
286        color.toggle_colorize_event()
287        self.assertIsNone(color.after_id)
288        self.assertTrue(color.colorizing)
289        self.assertFalse(color.stop_colorizing)
290        self.assertTrue(color.allow_colorizing)
291
292        # Toggle off while colorizing in progress.
293        color.toggle_colorize_event()
294        self.assertIsNone(color.after_id)
295        self.assertTrue(color.colorizing)
296        self.assertTrue(color.stop_colorizing)
297        self.assertFalse(color.allow_colorizing)
298
299        # Toggle on while colorizing not in progress.
300        color.colorizing = False
301        color.toggle_colorize_event()
302        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
303        self.assertFalse(color.colorizing)
304        self.assertTrue(color.stop_colorizing)
305        self.assertTrue(color.allow_colorizing)
306
307    @mock.patch.object(colorizer.ColorDelegator, 'recolorize_main')
308    def test_recolorize(self, mock_recmain):
309        text = self.text
310        color = self.color
311        eq = self.assertEqual
312        # Call recolorize manually and not scheduled.
313        text.after_cancel(color.after_id)
314
315        # No delegate.
316        save_delegate = color.delegate
317        color.delegate = None
318        color.recolorize()
319        mock_recmain.assert_not_called()
320        color.delegate = save_delegate
321
322        # Toggle off colorizing.
323        color.allow_colorizing = False
324        color.recolorize()
325        mock_recmain.assert_not_called()
326        color.allow_colorizing = True
327
328        # Colorizing in progress.
329        color.colorizing = True
330        color.recolorize()
331        mock_recmain.assert_not_called()
332        color.colorizing = False
333
334        # Colorizing is done, but not completed, so rescheduled.
335        color.recolorize()
336        self.assertFalse(color.stop_colorizing)
337        self.assertFalse(color.colorizing)
338        mock_recmain.assert_called()
339        eq(mock_recmain.call_count, 1)
340        # Rescheduled when TODO tag still exists.
341        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
342
343        # No changes to text, so no scheduling added.
344        text.tag_remove('TODO', '1.0', 'end')
345        color.recolorize()
346        self.assertFalse(color.stop_colorizing)
347        self.assertFalse(color.colorizing)
348        mock_recmain.assert_called()
349        eq(mock_recmain.call_count, 2)
350        self.assertIsNone(color.after_id)
351
352    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
353    def test_recolorize_main(self, mock_notify):
354        text = self.text
355        color = self.color
356        eq = self.assertEqual
357
358        text.insert('insert', source)
359        expected = (('1.0', ('KEYWORD',)), ('1.2', ()), ('1.3', ('KEYWORD',)),
360                    ('1.7', ()), ('1.9', ('BUILTIN',)), ('1.14', ('STRING',)),
361                    ('1.19', ('COMMENT',)),
362                    ('2.1', ('KEYWORD',)), ('2.18', ()), ('2.25', ('COMMENT',)),
363                    ('3.6', ('BUILTIN',)), ('3.12', ('KEYWORD',)), ('3.21', ('COMMENT',)),
364                    ('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()),
365                    ('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)),
366                    ('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
367                    ('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)),
368                    ('7.12', ()), ('7.14', ('STRING',)),
369                    # SYNC at the end of every line.
370                    ('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)),
371                   )
372
373        # Nothing marked to do therefore no tags in text.
374        text.tag_remove('TODO', '1.0', 'end')
375        color.recolorize_main()
376        for tag in text.tag_names():
377            with self.subTest(tag=tag):
378                eq(text.tag_ranges(tag), ())
379
380        # Source marked for processing.
381        text.tag_add('TODO', '1.0', 'end')
382        # Check some indexes.
383        color.recolorize_main()
384        for index, expected_tags in expected:
385            with self.subTest(index=index):
386                eq(text.tag_names(index), expected_tags)
387
388        # Check for some tags for ranges.
389        eq(text.tag_nextrange('TODO', '1.0'), ())
390        eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
391        eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43'))
392        eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0'))
393        eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53'))
394        eq(text.tag_nextrange('STRING', '7.0'), ('7.0', '7.3'))
395        eq(text.tag_nextrange('STRING', '7.3'), ('7.5', '7.12'))
396        eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17'))
397        eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26'))
398        eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0'))
399
400    @mock.patch.object(colorizer.ColorDelegator, 'recolorize')
401    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
402    def test_removecolors(self, mock_notify, mock_recolorize):
403        text = self.text
404        color = self.color
405        text.insert('insert', source)
406
407        color.recolorize_main()
408        # recolorize_main doesn't add these tags.
409        text.tag_add("ERROR", "1.0")
410        text.tag_add("TODO", "1.0")
411        text.tag_add("hit", "1.0")
412        for tag in color.tagdefs:
413            with self.subTest(tag=tag):
414                self.assertNotEqual(text.tag_ranges(tag), ())
415
416        color.removecolors()
417        for tag in color.tagdefs:
418            with self.subTest(tag=tag):
419                self.assertEqual(text.tag_ranges(tag), ())
420
421
422if __name__ == '__main__':
423    unittest.main(verbosity=2)
424