bpo-33628: IDLE: Minor code cleanup of codecontext.py and its tests (GH-7085)
This commit is contained in:
parent
8ebf5ceb0f
commit
8506016f90
@ -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.
|
bpo-32831: Add docstrings and tests for codecontext.py.
|
||||||
Coverage is 100%. Patch by Cheryl Sabella.
|
Coverage is 100%. Patch by Cheryl Sabella.
|
||||||
|
|
||||||
|
@ -23,9 +23,23 @@ UPDATEINTERVAL = 100 # millisec
|
|||||||
FONTUPDATEINTERVAL = 1000 # millisec
|
FONTUPDATEINTERVAL = 1000 # millisec
|
||||||
|
|
||||||
|
|
||||||
def getspacesfirstword(s, c=re.compile(r"^(\s*)(\w*)")):
|
def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
|
||||||
"Extract the beginning whitespace and first word from s."
|
"Extract the beginning whitespace and first word from codeline."
|
||||||
return c.match(s).groups()
|
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:
|
class CodeContext:
|
||||||
@ -42,12 +56,12 @@ class CodeContext:
|
|||||||
self.textfont is the editor window font.
|
self.textfont is the editor window font.
|
||||||
|
|
||||||
self.label displays the code context text above the editor text.
|
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.topvisible is the number of the top text line displayed.
|
||||||
self.info is a list of (line number, indent level, line text,
|
self.info is a list of (line number, indent level, line text,
|
||||||
block keyword) tuples for the block structure above topvisible.
|
block keyword) tuples for the block structure above topvisible.
|
||||||
s self.info[0] is initialized a 'dummy' line which
|
self.info[0] is initialized with a 'dummy' line which
|
||||||
# starts the toplevel 'block' of the module.
|
starts the toplevel 'block' of the module.
|
||||||
|
|
||||||
self.t1 and self.t2 are two timer events on the editor text widget to
|
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.
|
||||||
@ -94,23 +108,21 @@ class CodeContext:
|
|||||||
# All values are passed through getint(), since some
|
# All values are passed through getint(), since some
|
||||||
# values may be pixel objects, which can't simply be added to ints.
|
# values may be pixel objects, which can't simply be added to ints.
|
||||||
widgets = self.editwin.text, self.editwin.text_frame
|
widgets = self.editwin.text, self.editwin.text_frame
|
||||||
# Calculate the required vertical padding
|
# Calculate the required horizontal padding and border width.
|
||||||
padx = 0
|
padx = 0
|
||||||
|
border = 0
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
padx += widget.tk.getint(widget.pack_info()['padx'])
|
padx += widget.tk.getint(widget.pack_info()['padx'])
|
||||||
padx += widget.tk.getint(widget.cget('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'))
|
border += widget.tk.getint(widget.cget('border'))
|
||||||
self.label = tkinter.Label(
|
self.label = tkinter.Label(
|
||||||
self.editwin.top, text="\n" * (self.context_depth - 1),
|
self.editwin.top, text="\n" * (self.context_depth - 1),
|
||||||
anchor=W, justify=LEFT, font=self.textfont,
|
anchor=W, justify=LEFT, font=self.textfont,
|
||||||
bg=self.bgcolor, fg=self.fgcolor,
|
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)
|
padx=padx, border=border, relief=SUNKEN)
|
||||||
# Pack the label widget before and above the text_frame widget,
|
# 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,
|
self.label.pack(side=TOP, fill=X, expand=False,
|
||||||
before=self.editwin.text_frame)
|
before=self.editwin.text_frame)
|
||||||
else:
|
else:
|
||||||
@ -118,21 +130,6 @@ class CodeContext:
|
|||||||
self.label = None
|
self.label = None
|
||||||
return "break"
|
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):
|
def get_context(self, new_topvisible, stopline=1, stopindent=0):
|
||||||
"""Return a list of block line tuples and the 'last' indent.
|
"""Return a list of block line tuples and the 'last' indent.
|
||||||
|
|
||||||
@ -144,16 +141,17 @@ class CodeContext:
|
|||||||
"""
|
"""
|
||||||
assert stopline > 0
|
assert stopline > 0
|
||||||
lines = []
|
lines = []
|
||||||
# The indentation level we are currently in:
|
# The indentation level we are currently in.
|
||||||
lastindent = INFINITY
|
lastindent = INFINITY
|
||||||
# For a line to be interesting, it must begin with a block opening
|
# For a line to be interesting, it must begin with a block opening
|
||||||
# keyword, and have less indentation than lastindent.
|
# keyword, and have less indentation than lastindent.
|
||||||
for linenum in range(new_topvisible, stopline-1, -1):
|
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:
|
if indent < lastindent:
|
||||||
lastindent = indent
|
lastindent = indent
|
||||||
if opener in ("else", "elif"):
|
if opener in ("else", "elif"):
|
||||||
# We also show the if statement
|
# Also show the if statement.
|
||||||
lastindent += 1
|
lastindent += 1
|
||||||
if opener and linenum < new_topvisible and indent >= stopindent:
|
if opener and linenum < new_topvisible and indent >= stopindent:
|
||||||
lines.append((linenum, indent, text, opener))
|
lines.append((linenum, indent, text, opener))
|
||||||
@ -172,19 +170,19 @@ class CodeContext:
|
|||||||
the context label.
|
the context label.
|
||||||
"""
|
"""
|
||||||
new_topvisible = int(self.text.index("@0,0").split('.')[0])
|
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
|
return
|
||||||
if self.topvisible < new_topvisible: # scroll down
|
if self.topvisible < new_topvisible: # Scroll down.
|
||||||
lines, lastindent = self.get_context(new_topvisible,
|
lines, lastindent = self.get_context(new_topvisible,
|
||||||
self.topvisible)
|
self.topvisible)
|
||||||
# retain only context info applicable to the region
|
# Retain only context info applicable to the region
|
||||||
# between topvisible and new_topvisible:
|
# between topvisible and new_topvisible.
|
||||||
while self.info[-1][1] >= lastindent:
|
while self.info[-1][1] >= lastindent:
|
||||||
del self.info[-1]
|
del self.info[-1]
|
||||||
else: # self.topvisible > new_topvisible: # scroll up
|
else: # self.topvisible > new_topvisible: # Scroll up.
|
||||||
stopindent = self.info[-1][1] + 1
|
stopindent = self.info[-1][1] + 1
|
||||||
# retain only context info associated
|
# Retain only context info associated
|
||||||
# with lines above new_topvisible:
|
# with lines above new_topvisible.
|
||||||
while self.info[-1][0] >= new_topvisible:
|
while self.info[-1][0] >= new_topvisible:
|
||||||
stopindent = self.info[-1][1]
|
stopindent = self.info[-1][1]
|
||||||
del self.info[-1]
|
del self.info[-1]
|
||||||
@ -193,9 +191,9 @@ class CodeContext:
|
|||||||
stopindent)
|
stopindent)
|
||||||
self.info.extend(lines)
|
self.info.extend(lines)
|
||||||
self.topvisible = new_topvisible
|
self.topvisible = new_topvisible
|
||||||
# empty lines in context pane:
|
# Empty lines in context pane.
|
||||||
context_strings = [""] * max(0, self.context_depth - len(self.info))
|
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:]]
|
context_strings += [x[2] for x in self.info[-self.context_depth:]]
|
||||||
self.label["text"] = '\n'.join(context_strings)
|
self.label["text"] = '\n'.join(context_strings)
|
||||||
|
|
||||||
|
@ -96,8 +96,6 @@ class CodeContextTest(unittest.TestCase):
|
|||||||
eq(self.root.tk.call('after', 'info', self.cc.t2)[1], 'timer')
|
eq(self.root.tk.call('after', 'info', self.cc.t2)[1], 'timer')
|
||||||
|
|
||||||
def test_del(self):
|
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__()
|
self.cc.__del__()
|
||||||
with self.assertRaises(TclError) as msg:
|
with self.assertRaises(TclError) as msg:
|
||||||
self.root.tk.call('after', 'info', self.cc.t1)
|
self.root.tk.call('after', 'info', self.cc.t1)
|
||||||
@ -135,21 +133,6 @@ class CodeContextTest(unittest.TestCase):
|
|||||||
eq(toggle(), 'break')
|
eq(toggle(), 'break')
|
||||||
self.assertIsNone(cc.label)
|
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):
|
def test_get_context(self):
|
||||||
eq = self.assertEqual
|
eq = self.assertEqual
|
||||||
gc = self.cc.get_context
|
gc = self.cc.get_context
|
||||||
@ -323,8 +306,8 @@ class CodeContextTest(unittest.TestCase):
|
|||||||
|
|
||||||
class HelperFunctionText(unittest.TestCase):
|
class HelperFunctionText(unittest.TestCase):
|
||||||
|
|
||||||
def test_getspacesfirstword(self):
|
def test_get_spaces_firstword(self):
|
||||||
get = codecontext.getspacesfirstword
|
get = codecontext.get_spaces_firstword
|
||||||
test_lines = (
|
test_lines = (
|
||||||
(' first word', (' ', 'first')),
|
(' first word', (' ', 'first')),
|
||||||
('\tfirst word', ('\t', 'first')),
|
('\tfirst word', ('\t', 'first')),
|
||||||
@ -342,6 +325,24 @@ class HelperFunctionText(unittest.TestCase):
|
|||||||
c=re.compile(r'^(\s*)([^\s]*)')),
|
c=re.compile(r'^(\s*)([^\s]*)')),
|
||||||
(' ', '(continuation)'))
|
(' ', '(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__':
|
if __name__ == '__main__':
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
IDLE: Cleanup codecontext.py and its test.
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user