Skip to content

bpo-33628: IDLE: Minor code cleanup on codecontext #7085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Lib/idlelib/NEWS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
78 changes: 38 additions & 40 deletions Lib/idlelib/codecontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,23 @@
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:
Expand All @@ -42,15 +56,15 @@ def __init__(self, editwin):
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
Expand Down Expand Up @@ -94,45 +108,28 @@ def toggle_code_context_event(self, event=None):
# 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:
self.label.destroy()
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.

Expand All @@ -144,16 +141,17 @@ def get_context(self, new_topvisible, stopline=1, stopindent=0):
"""
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))
Expand All @@ -172,19 +170,19 @@ def update_code_context(self):
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]
Expand All @@ -193,9 +191,9 @@ def update_code_context(self):
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)

Expand Down
39 changes: 20 additions & 19 deletions Lib/idlelib/idle_test/test_codecontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ def test_init(self):
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)
Expand Down Expand Up @@ -135,21 +133,6 @@ def test_toggle_code_context_event(self):
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
Expand Down Expand Up @@ -323,8 +306,8 @@ def test_font_timer_event(self):

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')),
Expand All @@ -342,6 +325,24 @@ def test_getspacesfirstword(self):
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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
IDLE: Cleanup codecontext.py and its test.