bpo-35689: IDLE: Add docstrings and unittests for colorizer.py (GH-11472)

(cherry picked from commit ee0f927bd8)

Co-authored-by: Cheryl Sabella <cheryl.sabella@gmail.com>
This commit is contained in:
Miss Islington (bot) 2019-02-18 21:30:53 -08:00 committed by GitHub
parent 3129432845
commit 99e9c36b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 408 additions and 16 deletions

View File

@ -3,6 +3,8 @@ Released on 2019-??-??
======================================
bpo-35689: Add docstrings and tests for colorizer.
bpo-35833: Revise IDLE doc for control codes sent to Shell.
Add a code example block.

View File

@ -53,6 +53,21 @@ def color_config(text):
class ColorDelegator(Delegator):
"""Delegator for syntax highlighting (text coloring).
Class variables:
after_id: Identifier for scheduled after event.
allow_colorizing: Boolean toggle for applying colorizing.
colorizing: Boolean flag when colorizing is in process.
stop_colorizing: Boolean flag to end an active colorizing
process.
close_when_done: Widget to destroy after colorizing process
completes (doesn't seem to be used by IDLE).
Instance variables:
delegate: Delegator below this one in the stack, meaning the
one this one delegates to.
"""
def __init__(self):
Delegator.__init__(self)
@ -61,6 +76,16 @@ class ColorDelegator(Delegator):
self.LoadTagDefs()
def setdelegate(self, delegate):
"""Set the delegate for this instance.
A delegate is an instance of a Delegator class and each
delegate points to the next delegator in the stack. This
allows multiple delegators to be chained together for a
widget. The bottom delegate for a colorizer is a Text
widget.
If there is a delegate, also start the colorizing process.
"""
if self.delegate is not None:
self.unbind("<<toggle-auto-coloring>>")
Delegator.setdelegate(self, delegate)
@ -69,17 +94,18 @@ class ColorDelegator(Delegator):
self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event)
self.notify_range("1.0", "end")
else:
# No delegate - stop any colorizing
# No delegate - stop any colorizing.
self.stop_colorizing = True
self.allow_colorizing = False
def config_colors(self):
"Configure text widget tags with colors from tagdefs."
for tag, cnf in self.tagdefs.items():
if cnf:
self.tag_configure(tag, **cnf)
self.tag_raise('sel')
def LoadTagDefs(self):
"Create dictionary of tag names to text colors."
theme = idleConf.CurrentTheme()
self.tagdefs = {
"COMMENT": idleConf.GetHighlight(theme, "comment"),
@ -97,20 +123,24 @@ class ColorDelegator(Delegator):
if DEBUG: print('tagdefs',self.tagdefs)
def insert(self, index, chars, tags=None):
"Insert chars into widget at index and mark for colorizing."
index = self.index(index)
self.delegate.insert(index, chars, tags)
self.notify_range(index, index + "+%dc" % len(chars))
def delete(self, index1, index2=None):
"Delete chars between indexes and mark for colorizing."
index1 = self.index(index1)
self.delegate.delete(index1, index2)
self.notify_range(index1)
after_id = None
allow_colorizing = True
stop_colorizing = False
colorizing = False
def notify_range(self, index1, index2=None):
"Mark text changes for processing and restart colorizing, if active."
self.tag_add("TODO", index1, index2)
if self.after_id:
if DEBUG: print("colorizing already scheduled")
@ -121,8 +151,9 @@ class ColorDelegator(Delegator):
if self.allow_colorizing:
if DEBUG: print("schedule colorizing")
self.after_id = self.after(1, self.recolorize)
return
close_when_done = None # Window to be closed when done colorizing
close_when_done = None # Window to be closed when done colorizing.
def close(self, close_when_done=None):
if self.after_id:
@ -138,7 +169,14 @@ class ColorDelegator(Delegator):
else:
self.close_when_done = close_when_done
def toggle_colorize_event(self, event):
def toggle_colorize_event(self, event=None):
"""Toggle colorizing on and off.
When toggling off, if colorizing is scheduled or is in
process, it will be cancelled and/or stopped.
When toggling on, colorizing will be scheduled.
"""
if self.after_id:
after_id = self.after_id
self.after_id = None
@ -156,6 +194,17 @@ class ColorDelegator(Delegator):
return "break"
def recolorize(self):
"""Timer event (every 1ms) to colorize text.
Colorizing is only attempted when the text widget exists,
when colorizing is toggled on, and when the colorizing
process is not already running.
After colorizing is complete, some cleanup is done to
make sure that all the text has been colorized and to close
the window if the close event had been called while the
process was running.
"""
self.after_id = None
if not self.delegate:
if DEBUG: print("no delegate")
@ -185,6 +234,7 @@ class ColorDelegator(Delegator):
top.destroy()
def recolorize_main(self):
"Evaluate text and apply colorizing tags."
next = "1.0"
while True:
item = self.tag_nextrange("TODO", next)
@ -250,6 +300,7 @@ class ColorDelegator(Delegator):
return
def removecolors(self):
"Remove all colorizing tags."
for tag in self.tagdefs:
self.tag_remove(tag, "1.0", "end")
@ -273,7 +324,7 @@ def _color_delegator(parent): # htest #
"'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
"r'x', u'x', R'x', U'x', f'x', F'x'\n"
"fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
"b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x'.rB'x',Rb'x',RB'x'\n"
"b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'\n"
"# Invalid combinations of legal characters should be half colored.\n"
"ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'\n"
)

View File

@ -1,36 +1,103 @@
"Test colorizer, coverage 25%."
"Test colorizer, coverage 93%."
from idlelib import colorizer
from test.support import requires
from tkinter import Tk, Text
import unittest
from unittest import mock
from functools import partial
from tkinter import Tk, Text
from idlelib import config
from idlelib.percolator import Percolator
usercfg = colorizer.idleConf.userCfg
testcfg = {
'main': config.IdleUserConfParser(''),
'highlight': config.IdleUserConfParser(''),
'keys': config.IdleUserConfParser(''),
'extensions': config.IdleUserConfParser(''),
}
source = (
"if True: int ('1') # keyword, builtin, string, comment\n"
"elif False: print(0) # 'string' in comment\n"
"else: float(None) # if in comment\n"
"if iF + If + IF: 'keyword matching must respect case'\n"
"if'': x or'' # valid string-keyword no-space combinations\n"
"async def f(): await g()\n"
"'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
)
def setUpModule():
colorizer.idleConf.userCfg = testcfg
def tearDownModule():
colorizer.idleConf.userCfg = usercfg
class FunctionTest(unittest.TestCase):
def test_any(self):
self.assertTrue(colorizer.any('test', ('a', 'b')))
self.assertEqual(colorizer.any('test', ('a', 'b', 'cd')),
'(?P<test>a|b|cd)')
def test_make_pat(self):
# Tested in more detail by testing prog.
self.assertTrue(colorizer.make_pat())
def test_prog(self):
prog = colorizer.prog
eq = self.assertEqual
line = 'def f():\n print("hello")\n'
m = prog.search(line)
eq(m.groupdict()['KEYWORD'], 'def')
m = prog.search(line, m.end())
eq(m.groupdict()['SYNC'], '\n')
m = prog.search(line, m.end())
eq(m.groupdict()['BUILTIN'], 'print')
m = prog.search(line, m.end())
eq(m.groupdict()['STRING'], '"hello"')
m = prog.search(line, m.end())
eq(m.groupdict()['SYNC'], '\n')
def test_idprog(self):
idprog = colorizer.idprog
m = idprog.match('nospace')
self.assertIsNone(m)
m = idprog.match(' space')
self.assertEqual(m.group(0), ' space')
class ColorConfigTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.text = Text(cls.root)
root = cls.root = Tk()
root.withdraw()
cls.text = Text(root)
@classmethod
def tearDownClass(cls):
del cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_colorizer(self):
colorizer.color_config(self.text)
def test_color_config(self):
text = self.text
eq = self.assertEqual
colorizer.color_config(text)
# Uses IDLE Classic theme as default.
eq(text['background'], '#ffffff')
eq(text['foreground'], '#000000')
eq(text['selectbackground'], 'gray')
eq(text['selectforeground'], '#000000')
eq(text['insertbackground'], 'black')
eq(text['inactiveselectbackground'], 'gray')
class ColorDelegatorTest(unittest.TestCase):
@ -38,15 +105,286 @@ class ColorDelegatorTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
root = cls.root = Tk()
root.withdraw()
text = cls.text = Text(root)
cls.percolator = Percolator(text)
# Delegator stack = [Delagator(text)]
@classmethod
def tearDownClass(cls):
cls.percolator.redir.close()
del cls.percolator, cls.text
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_colorizer(self):
colorizer.ColorDelegator()
def setUp(self):
self.color = colorizer.ColorDelegator()
self.percolator.insertfilter(self.color)
# Calls color.setdelagate(Delagator(text)).
def tearDown(self):
self.color.close()
self.percolator.removefilter(self.color)
self.text.delete('1.0', 'end')
self.color.resetcache()
del self.color
def test_init(self):
color = self.color
self.assertIsInstance(color, colorizer.ColorDelegator)
# The following are class variables.
self.assertTrue(color.allow_colorizing)
self.assertFalse(color.colorizing)
def test_setdelegate(self):
# Called in setUp.
color = self.color
self.assertIsInstance(color.delegate, colorizer.Delegator)
# It is too late to mock notify_range, so test side effect.
self.assertEqual(self.root.tk.call(
'after', 'info', color.after_id)[1], 'timer')
def test_LoadTagDefs(self):
highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
for tag, colors in self.color.tagdefs.items():
with self.subTest(tag=tag):
self.assertIn('background', colors)
self.assertIn('foreground', colors)
if tag not in ('SYNC', 'TODO'):
self.assertEqual(colors, highlight(element=tag.lower()))
def test_config_colors(self):
text = self.text
highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
for tag in self.color.tagdefs:
for plane in ('background', 'foreground'):
with self.subTest(tag=tag, plane=plane):
if tag in ('SYNC', 'TODO'):
self.assertEqual(text.tag_cget(tag, plane), '')
else:
self.assertEqual(text.tag_cget(tag, plane),
highlight(element=tag.lower())[plane])
# 'sel' is marked as the highest priority.
self.assertEqual(text.tag_names()[-1], 'sel')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_insert(self, mock_notify):
text = self.text
# Initial text.
text.insert('insert', 'foo')
self.assertEqual(text.get('1.0', 'end'), 'foo\n')
mock_notify.assert_called_with('1.0', '1.0+3c')
# Additional text.
text.insert('insert', 'barbaz')
self.assertEqual(text.get('1.0', 'end'), 'foobarbaz\n')
mock_notify.assert_called_with('1.3', '1.3+6c')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_delete(self, mock_notify):
text = self.text
# Initialize text.
text.insert('insert', 'abcdefghi')
self.assertEqual(text.get('1.0', 'end'), 'abcdefghi\n')
# Delete single character.
text.delete('1.7')
self.assertEqual(text.get('1.0', 'end'), 'abcdefgi\n')
mock_notify.assert_called_with('1.7')
# Delete multiple characters.
text.delete('1.3', '1.6')
self.assertEqual(text.get('1.0', 'end'), 'abcgi\n')
mock_notify.assert_called_with('1.3')
def test_notify_range(self):
text = self.text
color = self.color
eq = self.assertEqual
# Colorizing already scheduled.
save_id = color.after_id
eq(self.root.tk.call('after', 'info', save_id)[1], 'timer')
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
# Coloring scheduled and colorizing in progress.
color.colorizing = True
color.notify_range('1.0', 'end')
self.assertFalse(color.stop_colorizing)
eq(color.after_id, save_id)
# No colorizing scheduled and colorizing in progress.
text.after_cancel(save_id)
color.after_id = None
color.notify_range('1.0', '1.0+3c')
self.assertTrue(color.stop_colorizing)
self.assertIsNotNone(color.after_id)
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
# New event scheduled.
self.assertNotEqual(color.after_id, save_id)
# No colorizing scheduled and colorizing off.
text.after_cancel(color.after_id)
color.after_id = None
color.allow_colorizing = False
color.notify_range('1.4', '1.4+10c')
# Nothing scheduled when colorizing is off.
self.assertIsNone(color.after_id)
def test_toggle_colorize_event(self):
color = self.color
eq = self.assertEqual
# Starts with colorizing allowed and scheduled.
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
# Toggle colorizing off.
color.toggle_colorize_event()
self.assertIsNone(color.after_id)
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertFalse(color.allow_colorizing)
# Toggle on while colorizing in progress (doesn't add timer).
color.colorizing = True
color.toggle_colorize_event()
self.assertIsNone(color.after_id)
self.assertTrue(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
# Toggle off while colorizing in progress.
color.toggle_colorize_event()
self.assertIsNone(color.after_id)
self.assertTrue(color.colorizing)
self.assertTrue(color.stop_colorizing)
self.assertFalse(color.allow_colorizing)
# Toggle on while colorizing not in progress.
color.colorizing = False
color.toggle_colorize_event()
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
self.assertFalse(color.colorizing)
self.assertTrue(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
@mock.patch.object(colorizer.ColorDelegator, 'recolorize_main')
def test_recolorize(self, mock_recmain):
text = self.text
color = self.color
eq = self.assertEqual
# Call recolorize manually and not scheduled.
text.after_cancel(color.after_id)
# No delegate.
save_delegate = color.delegate
color.delegate = None
color.recolorize()
mock_recmain.assert_not_called()
color.delegate = save_delegate
# Toggle off colorizing.
color.allow_colorizing = False
color.recolorize()
mock_recmain.assert_not_called()
color.allow_colorizing = True
# Colorizing in progress.
color.colorizing = True
color.recolorize()
mock_recmain.assert_not_called()
color.colorizing = False
# Colorizing is done, but not completed, so rescheduled.
color.recolorize()
self.assertFalse(color.stop_colorizing)
self.assertFalse(color.colorizing)
mock_recmain.assert_called()
eq(mock_recmain.call_count, 1)
# Rescheduled when TODO tag still exists.
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
# No changes to text, so no scheduling added.
text.tag_remove('TODO', '1.0', 'end')
color.recolorize()
self.assertFalse(color.stop_colorizing)
self.assertFalse(color.colorizing)
mock_recmain.assert_called()
eq(mock_recmain.call_count, 2)
self.assertIsNone(color.after_id)
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_recolorize_main(self, mock_notify):
text = self.text
color = self.color
eq = self.assertEqual
text.insert('insert', source)
expected = (('1.0', ('KEYWORD',)), ('1.2', ()), ('1.3', ('KEYWORD',)),
('1.7', ()), ('1.9', ('BUILTIN',)), ('1.14', ('STRING',)),
('1.19', ('COMMENT',)),
('2.1', ('KEYWORD',)), ('2.18', ()), ('2.25', ('COMMENT',)),
('3.6', ('BUILTIN',)), ('3.12', ('KEYWORD',)), ('3.21', ('COMMENT',)),
('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()),
('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)),
('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)),
('7.12', ()), ('7.14', ('STRING',)),
# SYNC at the end of every line.
('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)),
)
# Nothing marked to do therefore no tags in text.
text.tag_remove('TODO', '1.0', 'end')
color.recolorize_main()
for tag in text.tag_names():
with self.subTest(tag=tag):
eq(text.tag_ranges(tag), ())
# Source marked for processing.
text.tag_add('TODO', '1.0', 'end')
# Check some indexes.
color.recolorize_main()
for index, expected_tags in expected:
with self.subTest(index=index):
eq(text.tag_names(index), expected_tags)
# Check for some tags for ranges.
eq(text.tag_nextrange('TODO', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43'))
eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0'))
eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53'))
eq(text.tag_nextrange('STRING', '7.0'), ('7.0', '7.3'))
eq(text.tag_nextrange('STRING', '7.3'), ('7.5', '7.12'))
eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17'))
eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26'))
eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0'))
@mock.patch.object(colorizer.ColorDelegator, 'recolorize')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
def test_removecolors(self, mock_notify, mock_recolorize):
text = self.text
color = self.color
text.insert('insert', source)
color.recolorize_main()
# recolorize_main doesn't add these tags.
text.tag_add("ERROR", "1.0")
text.tag_add("TODO", "1.0")
text.tag_add("hit", "1.0")
for tag in color.tagdefs:
with self.subTest(tag=tag):
self.assertNotEqual(text.tag_ranges(tag), ())
color.removecolors()
for tag in color.tagdefs:
with self.subTest(tag=tag):
self.assertEqual(text.tag_ranges(tag), ())
if __name__ == '__main__':

View File

@ -0,0 +1 @@
Add docstrings and unittests for colorizer.py.