diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt index 786378e2a63..2f8237d2395 100644 --- a/Lib/idlelib/NEWS.txt +++ b/Lib/idlelib/NEWS.txt @@ -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. diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py index efd163ed265..635f68c86e1 100644 --- a/Lib/idlelib/codecontext.py +++ b/Lib/idlelib/codecontext.py @@ -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 <>. + Initially None, it is toggled via <>. 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) diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py index 448094eda7e..ed45828641b 100644 --- a/Lib/idlelib/idle_test/test_codecontext.py +++ b/Lib/idlelib/idle_test/test_codecontext.py @@ -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) diff --git a/Misc/NEWS.d/next/IDLE/2018-05-23-19-51-07.bpo-33628.sLlFLO.rst b/Misc/NEWS.d/next/IDLE/2018-05-23-19-51-07.bpo-33628.sLlFLO.rst new file mode 100644 index 00000000000..f0b13a21c34 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2018-05-23-19-51-07.bpo-33628.sLlFLO.rst @@ -0,0 +1,2 @@ +IDLE: Cleanup codecontext.py and its test. +