bpo-33628: IDLE: Minor code cleanup of codecontext.py and its tests (GH-7085)

This commit is contained in:
Cheryl Sabella 2018-05-23 22:18:15 -04:00 committed by Terry Jan Reedy
parent 8ebf5ceb0f
commit 8506016f90
4 changed files with 62 additions and 59 deletions

View File

@ -3,6 +3,8 @@ Released on 2018-06-18?
======================================
bpo-33628: Cleanup codecontext.py and its test.
bpo-32831: Add docstrings and tests for codecontext.py.
Coverage is 100%. Patch by Cheryl Sabella.

View File

@ -23,9 +23,23 @@ UPDATEINTERVAL = 100 # millisec
FONTUPDATEINTERVAL = 1000 # millisec
def getspacesfirstword(s, c=re.compile(r"^(\s*)(\w*)")):
"Extract the beginning whitespace and first word from s."
return c.match(s).groups()
def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
"Extract the beginning whitespace and first word from codeline."
return c.match(codeline).groups()
def get_line_info(codeline):
"""Return tuple of (line indent value, codeline, block start keyword).
The indentation of empty lines (or comment lines) is INFINITY.
If the line does not start a block, the keyword value is False.
"""
spaces, firstword = get_spaces_firstword(codeline)
indent = len(spaces)
if len(codeline) == indent or codeline[indent] == '#':
indent = INFINITY
opener = firstword in BLOCKOPENERS and firstword
return indent, codeline, opener
class CodeContext:
@ -42,15 +56,15 @@ class CodeContext:
self.textfont is the editor window font.
self.label displays the code context text above the editor text.
Initially None it is toggled via <<toggle-code-context>>.
Initially None, it is toggled via <<toggle-code-context>>.
self.topvisible is the number of the top text line displayed.
self.info is a list of (line number, indent level, line text,
block keyword) tuples for the block structure above topvisible.
s self.info[0] is initialized a 'dummy' line which
# starts the toplevel 'block' of the module.
self.info[0] is initialized with a 'dummy' line which
starts the toplevel 'block' of the module.
self.t1 and self.t2 are two timer events on the editor text widget to
monitor for changes to the context text or editor font.
monitor for changes to the context text or editor font.
"""
self.editwin = editwin
self.text = editwin.text
@ -94,23 +108,21 @@ class CodeContext:
# All values are passed through getint(), since some
# values may be pixel objects, which can't simply be added to ints.
widgets = self.editwin.text, self.editwin.text_frame
# Calculate the required vertical padding
# Calculate the required horizontal padding and border width.
padx = 0
border = 0
for widget in widgets:
padx += widget.tk.getint(widget.pack_info()['padx'])
padx += widget.tk.getint(widget.cget('padx'))
# Calculate the required border width
border = 0
for widget in widgets:
border += widget.tk.getint(widget.cget('border'))
self.label = tkinter.Label(
self.editwin.top, text="\n" * (self.context_depth - 1),
anchor=W, justify=LEFT, font=self.textfont,
bg=self.bgcolor, fg=self.fgcolor,
width=1, #don't request more than we get
width=1, # Don't request more than we get.
padx=padx, border=border, relief=SUNKEN)
# Pack the label widget before and above the text_frame widget,
# thus ensuring that it will appear directly above text_frame
# thus ensuring that it will appear directly above text_frame.
self.label.pack(side=TOP, fill=X, expand=False,
before=self.editwin.text_frame)
else:
@ -118,21 +130,6 @@ class CodeContext:
self.label = None
return "break"
def get_line_info(self, linenum):
"""Return tuple of (line indent value, text, and block start keyword).
If the line does not start a block, the keyword value is False.
The indentation of empty lines (or comment lines) is INFINITY.
"""
text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
spaces, firstword = getspacesfirstword(text)
opener = firstword in BLOCKOPENERS and firstword
if len(text) == len(spaces) or text[len(spaces)] == '#':
indent = INFINITY
else:
indent = len(spaces)
return indent, text, opener
def get_context(self, new_topvisible, stopline=1, stopindent=0):
"""Return a list of block line tuples and the 'last' indent.
@ -144,16 +141,17 @@ class CodeContext:
"""
assert stopline > 0
lines = []
# The indentation level we are currently in:
# The indentation level we are currently in.
lastindent = INFINITY
# For a line to be interesting, it must begin with a block opening
# keyword, and have less indentation than lastindent.
for linenum in range(new_topvisible, stopline-1, -1):
indent, text, opener = self.get_line_info(linenum)
codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
indent, text, opener = get_line_info(codeline)
if indent < lastindent:
lastindent = indent
if opener in ("else", "elif"):
# We also show the if statement
# Also show the if statement.
lastindent += 1
if opener and linenum < new_topvisible and indent >= stopindent:
lines.append((linenum, indent, text, opener))
@ -172,19 +170,19 @@ class CodeContext:
the context label.
"""
new_topvisible = int(self.text.index("@0,0").split('.')[0])
if self.topvisible == new_topvisible: # haven't scrolled
if self.topvisible == new_topvisible: # Haven't scrolled.
return
if self.topvisible < new_topvisible: # scroll down
if self.topvisible < new_topvisible: # Scroll down.
lines, lastindent = self.get_context(new_topvisible,
self.topvisible)
# retain only context info applicable to the region
# between topvisible and new_topvisible:
# Retain only context info applicable to the region
# between topvisible and new_topvisible.
while self.info[-1][1] >= lastindent:
del self.info[-1]
else: # self.topvisible > new_topvisible: # scroll up
else: # self.topvisible > new_topvisible: # Scroll up.
stopindent = self.info[-1][1] + 1
# retain only context info associated
# with lines above new_topvisible:
# Retain only context info associated
# with lines above new_topvisible.
while self.info[-1][0] >= new_topvisible:
stopindent = self.info[-1][1]
del self.info[-1]
@ -193,9 +191,9 @@ class CodeContext:
stopindent)
self.info.extend(lines)
self.topvisible = new_topvisible
# empty lines in context pane:
# Empty lines in context pane.
context_strings = [""] * max(0, self.context_depth - len(self.info))
# followed by the context hint lines:
# Followed by the context hint lines.
context_strings += [x[2] for x in self.info[-self.context_depth:]]
self.label["text"] = '\n'.join(context_strings)

View File

@ -96,8 +96,6 @@ class CodeContextTest(unittest.TestCase):
eq(self.root.tk.call('after', 'info', self.cc.t2)[1], 'timer')
def test_del(self):
self.root.tk.call('after', 'info', self.cc.t1)
self.root.tk.call('after', 'info', self.cc.t2)
self.cc.__del__()
with self.assertRaises(TclError) as msg:
self.root.tk.call('after', 'info', self.cc.t1)
@ -135,21 +133,6 @@ class CodeContextTest(unittest.TestCase):
eq(toggle(), 'break')
self.assertIsNone(cc.label)
def test_get_line_info(self):
eq = self.assertEqual
gli = self.cc.get_line_info
# Line 1 is not a BLOCKOPENER.
eq(gli(1), (codecontext.INFINITY, '', False))
# Line 2 is a BLOCKOPENER without an indent.
eq(gli(2), (0, 'class C1():', 'class'))
# Line 3 is not a BLOCKOPENER and does not return the indent level.
eq(gli(3), (codecontext.INFINITY, ' # Class comment.', False))
# Line 4 is a BLOCKOPENER and is indented.
eq(gli(4), (4, ' def __init__(self, a, b):', 'def'))
# Line 8 is a different BLOCKOPENER and is indented.
eq(gli(8), (8, ' if a > b:', 'if'))
def test_get_context(self):
eq = self.assertEqual
gc = self.cc.get_context
@ -323,8 +306,8 @@ class CodeContextTest(unittest.TestCase):
class HelperFunctionText(unittest.TestCase):
def test_getspacesfirstword(self):
get = codecontext.getspacesfirstword
def test_get_spaces_firstword(self):
get = codecontext.get_spaces_firstword
test_lines = (
(' first word', (' ', 'first')),
('\tfirst word', ('\t', 'first')),
@ -342,6 +325,24 @@ class HelperFunctionText(unittest.TestCase):
c=re.compile(r'^(\s*)([^\s]*)')),
(' ', '(continuation)'))
def test_get_line_info(self):
eq = self.assertEqual
gli = codecontext.get_line_info
lines = code_sample.splitlines()
# Line 1 is not a BLOCKOPENER.
eq(gli(lines[0]), (codecontext.INFINITY, '', False))
# Line 2 is a BLOCKOPENER without an indent.
eq(gli(lines[1]), (0, 'class C1():', 'class'))
# Line 3 is not a BLOCKOPENER and does not return the indent level.
eq(gli(lines[2]), (codecontext.INFINITY, ' # Class comment.', False))
# Line 4 is a BLOCKOPENER and is indented.
eq(gli(lines[3]), (4, ' def __init__(self, a, b):', 'def'))
# Line 8 is a different BLOCKOPENER and is indented.
eq(gli(lines[7]), (8, ' if a > b:', 'if'))
# Test tab.
eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if'))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -0,0 +1,2 @@
IDLE: Cleanup codecontext.py and its test.