[Python-checkins] bpo-32831: IDLE: Add docstrings and tests for codecontext (GH-5638)

Miss Islington (bot) webhook-mailer at python.org
Sat May 19 16:28:06 EDT 2018


https://github.com/python/cpython/commit/0efa1353b756bd61d4e852ff4ef146735bef5522
commit: 0efa1353b756bd61d4e852ff4ef146735bef5522
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2018-05-19T13:28:03-07:00
summary:

bpo-32831: IDLE: Add docstrings and tests for codecontext (GH-5638)

(cherry picked from commit 654038d896d78a8373b60184f335acd516215acd)

Co-authored-by: Cheryl Sabella <cheryl.sabella at gmail.com>

files:
A Lib/idlelib/idle_test/test_codecontext.py
A Misc/NEWS.d/next/IDLE/2018-02-12-08-08-45.bpo-32831.srDRvU.rst
M Lib/idlelib/codecontext.py

diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py
index 2bfb2e988ff4..efd163ed265e 100644
--- a/Lib/idlelib/codecontext.py
+++ b/Lib/idlelib/codecontext.py
@@ -22,32 +22,49 @@
 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()
 
 
 class CodeContext:
+    "Display block context above the edit window."
+
     bgcolor = "LightGray"
     fgcolor = "Black"
 
     def __init__(self, editwin):
+        """Initialize settings for context block.
+
+        editwin is the Editor window for the context block.
+        self.text is the editor window text widget.
+        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>>.
+        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.t1 and self.t2 are two timer events on the editor text widget to
+        monitor for changes to the context text or editor font.
+        """
         self.editwin = editwin
         self.text = editwin.text
         self.textfont = self.text["font"]
         self.label = None
-        # self.info is a list of (line number, indent level, line text, block
-        # keyword) tuples providing the block structure associated with
-        # self.topvisible (the linenumber of the line displayed at the top of
-        # the edit window). self.info[0] is initialized as a 'dummy' line which
-        # starts the toplevel 'block' of the module.
-        self.info = [(0, -1, "", False)]
         self.topvisible = 1
+        self.info = [(0, -1, "", False)]
         # Start two update cycles, one for context lines, one for font changes.
         self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
         self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
 
     @classmethod
     def reload(cls):
+        "Load class variables from config."
         cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
                                        "numlines", type="int", default=3)
 ##        cls.bgcolor = idleConf.GetOption("extensions", "CodeContext",
@@ -56,6 +73,7 @@ def reload(cls):
 ##                                     "fgcolor", type="str", default="Black")
 
     def __del__(self):
+        "Cancel scheduled events."
         try:
             self.text.after_cancel(self.t1)
             self.text.after_cancel(self.t2)
@@ -63,6 +81,12 @@ def __del__(self):
             pass
 
     def toggle_code_context_event(self, event=None):
+        """Toggle code context display.
+
+        If self.label doesn't exist, create it to match the size of the editor
+        window text (toggle on).  If it does exist, destroy it (toggle off).
+        Return 'break' to complete the processing of the binding.
+        """
         if not self.label:
             # Calculate the border width and horizontal padding required to
             # align the context with the text in the main Text widget.
@@ -95,11 +119,10 @@ def toggle_code_context_event(self, event=None):
         return "break"
 
     def get_line_info(self, linenum):
-        """Get the line indent value, text, and any block start keyword
+        """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)
@@ -111,11 +134,13 @@ def get_line_info(self, linenum):
         return indent, text, opener
 
     def get_context(self, new_topvisible, stopline=1, stopindent=0):
-        """Get context lines, starting at new_topvisible and working backwards.
-
-        Stop when stopline or stopindent is reached. Return a tuple of context
-        data and the indent level at the top of the region inspected.
+        """Return a list of block line tuples and the 'last' indent.
 
+        The tuple fields are (linenum, indent, text, opener).
+        The list represents header lines from new_topvisible back to
+        stopline with successively shorter indents > stopindent.
+        The list is returned ordered by line number.
+        Last indent returned is the smallest indent observed.
         """
         assert stopline > 0
         lines = []
@@ -140,6 +165,11 @@ def get_context(self, new_topvisible, stopline=1, stopindent=0):
     def update_code_context(self):
         """Update context information and lines visible in the context pane.
 
+        No update is done if the text hasn't been scrolled.  If the text
+        was scrolled, the lines that should be shown in the context will
+        be retrieved and the label widget will be updated with the code,
+        padded with blank lines so that the code appears on the bottom of
+        the context label.
         """
         new_topvisible = int(self.text.index("@0,0").split('.')[0])
         if self.topvisible == new_topvisible:      # haven't scrolled
@@ -151,7 +181,7 @@ def update_code_context(self):
             # between topvisible and new_topvisible:
             while self.info[-1][1] >= lastindent:
                 del self.info[-1]
-        elif 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:
@@ -170,11 +200,13 @@ def update_code_context(self):
         self.label["text"] = '\n'.join(context_strings)
 
     def timer_event(self):
+        "Event on editor text widget triggered every UPDATEINTERVAL ms."
         if self.label:
             self.update_code_context()
         self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
 
     def font_timer_event(self):
+        "Event on editor text widget triggered every FONTUPDATEINTERVAL ms."
         newtextfont = self.text["font"]
         if self.label and newtextfont != self.textfont:
             self.textfont = newtextfont
@@ -183,3 +215,8 @@ def font_timer_event(self):
 
 
 CodeContext.reload()
+
+
+if __name__ == "__main__":  # pragma: no cover
+    import unittest
+    unittest.main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py
new file mode 100644
index 000000000000..448094eda7ef
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_codecontext.py
@@ -0,0 +1,347 @@
+"""Test idlelib.codecontext.
+
+Coverage: 100%
+"""
+
+import re
+
+import unittest
+from unittest import mock
+from test.support import requires
+from tkinter import Tk, Frame, Text, TclError
+
+import idlelib.codecontext as codecontext
+from idlelib import config
+
+
+usercfg = codecontext.idleConf.userCfg
+testcfg = {
+    'main': config.IdleUserConfParser(''),
+    'highlight': config.IdleUserConfParser(''),
+    'keys': config.IdleUserConfParser(''),
+    'extensions': config.IdleUserConfParser(''),
+}
+code_sample = """\
+
+class C1():
+    # Class comment.
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+    def compare(self):
+        if a > b:
+            return a
+        elif a < b:
+            return b
+        else:
+            return None
+"""
+
+
+class DummyEditwin:
+    def __init__(self, root, frame, text):
+        self.root = root
+        self.top = root
+        self.text_frame = frame
+        self.text = text
+
+
+class CodeContextTest(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        requires('gui')
+        root = cls.root = Tk()
+        root.withdraw()
+        frame = cls.frame = Frame(root)
+        text = cls.text = Text(frame)
+        text.insert('1.0', code_sample)
+        # Need to pack for creation of code context label widget.
+        frame.pack(side='left', fill='both', expand=1)
+        text.pack(side='top', fill='both', expand=1)
+        cls.editor = DummyEditwin(root, frame, text)
+        codecontext.idleConf.userCfg = testcfg
+
+    @classmethod
+    def tearDownClass(cls):
+        codecontext.idleConf.userCfg = usercfg
+        cls.editor.text.delete('1.0', 'end')
+        del cls.editor, cls.frame, cls.text
+        cls.root.update_idletasks()
+        cls.root.destroy()
+        del cls.root
+
+    def setUp(self):
+        self.cc = codecontext.CodeContext(self.editor)
+
+    def tearDown(self):
+        if self.cc.label:
+            self.cc.label.destroy()
+        # Explicitly call __del__ to remove scheduled scripts.
+        self.cc.__del__()
+        del self.cc.label, self.cc
+
+    def test_init(self):
+        eq = self.assertEqual
+        ed = self.editor
+        cc = self.cc
+
+        eq(cc.editwin, ed)
+        eq(cc.text, ed.text)
+        eq(cc.textfont, ed.text['font'])
+        self.assertIsNone(cc.label)
+        eq(cc.info, [(0, -1, '', False)])
+        eq(cc.topvisible, 1)
+        eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
+        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)
+            self.assertIn("doesn't exist", msg)
+        with self.assertRaises(TclError) as msg:
+            self.root.tk.call('after', 'info', self.cc.t2)
+            self.assertIn("doesn't exist", msg)
+        # For coverage on the except.  Have to delete because the
+        # above Tcl error is caught by after_cancel.
+        del self.cc.t1, self.cc.t2
+        self.cc.__del__()
+
+    def test_reload(self):
+        codecontext.CodeContext.reload()
+        self.assertEqual(self.cc.context_depth, 3)
+
+    def test_toggle_code_context_event(self):
+        eq = self.assertEqual
+        cc = self.cc
+        toggle = cc.toggle_code_context_event
+
+        # Make sure code context is off.
+        if cc.label:
+            toggle()
+
+        # Toggle on.
+        eq(toggle(), 'break')
+        self.assertIsNotNone(cc.label)
+        eq(cc.label['font'], cc.textfont)
+        eq(cc.label['fg'], cc.fgcolor)
+        eq(cc.label['bg'], cc.bgcolor)
+        eq(cc.label['text'], '\n' * 2)
+
+        # Toggle off.
+        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
+
+        # stopline must be greater than 0.
+        with self.assertRaises(AssertionError):
+            gc(1, stopline=0)
+
+        eq(gc(3), ([(2, 0, 'class C1():', 'class')], 0))
+
+        # Don't return comment.
+        eq(gc(4), ([(2, 0, 'class C1():', 'class')], 0))
+
+        # Two indentation levels and no comment.
+        eq(gc(5), ([(2, 0, 'class C1():', 'class'),
+                    (4, 4, '    def __init__(self, a, b):', 'def')], 0))
+
+        # Only one 'def' is returned, not both at the same indent level.
+        eq(gc(10), ([(2, 0, 'class C1():', 'class'),
+                     (7, 4, '    def compare(self):', 'def'),
+                     (8, 8, '        if a > b:', 'if')], 0))
+
+        # With 'elif', also show the 'if' even though it's at the same level.
+        eq(gc(11), ([(2, 0, 'class C1():', 'class'),
+                     (7, 4, '    def compare(self):', 'def'),
+                     (8, 8, '        if a > b:', 'if'),
+                     (10, 8, '        elif a < b:', 'elif')], 0))
+
+        # Set stop_line to not go back to first line in source code.
+        # Return includes stop_line.
+        eq(gc(11, stopline=2), ([(2, 0, 'class C1():', 'class'),
+                                 (7, 4, '    def compare(self):', 'def'),
+                                 (8, 8, '        if a > b:', 'if'),
+                                 (10, 8, '        elif a < b:', 'elif')], 0))
+        eq(gc(11, stopline=3), ([(7, 4, '    def compare(self):', 'def'),
+                                 (8, 8, '        if a > b:', 'if'),
+                                 (10, 8, '        elif a < b:', 'elif')], 4))
+        eq(gc(11, stopline=8), ([(8, 8, '        if a > b:', 'if'),
+                                 (10, 8, '        elif a < b:', 'elif')], 8))
+
+        # Set stop_indent to test indent level to stop at.
+        eq(gc(11, stopindent=4), ([(7, 4, '    def compare(self):', 'def'),
+                                   (8, 8, '        if a > b:', 'if'),
+                                   (10, 8, '        elif a < b:', 'elif')], 4))
+        # Check that the 'if' is included.
+        eq(gc(11, stopindent=8), ([(8, 8, '        if a > b:', 'if'),
+                                   (10, 8, '        elif a < b:', 'elif')], 8))
+
+    def test_update_code_context(self):
+        eq = self.assertEqual
+        cc = self.cc
+        # Ensure code context is active.
+        if not cc.label:
+            cc.toggle_code_context_event()
+
+        # Invoke update_code_context without scrolling - nothing happens.
+        self.assertIsNone(cc.update_code_context())
+        eq(cc.info, [(0, -1, '', False)])
+        eq(cc.topvisible, 1)
+
+        # Scroll down to line 2.
+        cc.text.yview(2)
+        cc.update_code_context()
+        eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')])
+        eq(cc.topvisible, 3)
+        # context_depth is 3 so it pads with blank lines.
+        eq(cc.label['text'], '\n'
+                             '\n'
+                             'class C1():')
+
+        # Scroll down to line 3.  Since it's a comment, nothing changes.
+        cc.text.yview(3)
+        cc.update_code_context()
+        eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')])
+        eq(cc.topvisible, 4)
+        eq(cc.label['text'], '\n'
+                             '\n'
+                             'class C1():')
+
+        # Scroll down to line 4.
+        cc.text.yview(4)
+        cc.update_code_context()
+        eq(cc.info, [(0, -1, '', False),
+                     (2, 0, 'class C1():', 'class'),
+                     (4, 4, '    def __init__(self, a, b):', 'def')])
+        eq(cc.topvisible, 5)
+        eq(cc.label['text'], '\n'
+                             'class C1():\n'
+                             '    def __init__(self, a, b):')
+
+        # Scroll down to line 11.  Last 'def' is removed.
+        cc.text.yview(11)
+        cc.update_code_context()
+        eq(cc.info, [(0, -1, '', False),
+                     (2, 0, 'class C1():', 'class'),
+                     (7, 4, '    def compare(self):', 'def'),
+                     (8, 8, '        if a > b:', 'if'),
+                     (10, 8, '        elif a < b:', 'elif')])
+        eq(cc.topvisible, 12)
+        eq(cc.label['text'], '    def compare(self):\n'
+                             '        if a > b:\n'
+                             '        elif a < b:')
+
+        # No scroll.  No update, even though context_depth changed.
+        cc.update_code_context()
+        cc.context_depth = 1
+        eq(cc.info, [(0, -1, '', False),
+                     (2, 0, 'class C1():', 'class'),
+                     (7, 4, '    def compare(self):', 'def'),
+                     (8, 8, '        if a > b:', 'if'),
+                     (10, 8, '        elif a < b:', 'elif')])
+        eq(cc.topvisible, 12)
+        eq(cc.label['text'], '    def compare(self):\n'
+                             '        if a > b:\n'
+                             '        elif a < b:')
+
+        # Scroll up.
+        cc.text.yview(5)
+        cc.update_code_context()
+        eq(cc.info, [(0, -1, '', False),
+                     (2, 0, 'class C1():', 'class'),
+                     (4, 4, '    def __init__(self, a, b):', 'def')])
+        eq(cc.topvisible, 6)
+        # context_depth is 1.
+        eq(cc.label['text'], '    def __init__(self, a, b):')
+
+    @mock.patch.object(codecontext.CodeContext, 'update_code_context')
+    def test_timer_event(self, mock_update):
+        # Ensure code context is not active.
+        if self.cc.label:
+            self.cc.toggle_code_context_event()
+        self.cc.timer_event()
+        mock_update.assert_not_called()
+
+        # Activate code context.
+        self.cc.toggle_code_context_event()
+        self.cc.timer_event()
+        mock_update.assert_called()
+
+    def test_font_timer_event(self):
+        eq = self.assertEqual
+        cc = self.cc
+        save_font = cc.text['font']
+        test_font = 'FakeFont'
+
+        # Ensure code context is not active.
+        if cc.label:
+            cc.toggle_code_context_event()
+
+        # Nothing updates on inactive code context.
+        cc.text['font'] = test_font
+        cc.font_timer_event()
+        eq(cc.textfont, save_font)
+
+        # Activate code context, but no change to font.
+        cc.toggle_code_context_event()
+        cc.text['font'] = save_font
+        cc.font_timer_event()
+        eq(cc.textfont, save_font)
+        eq(cc.label['font'], save_font)
+
+        # Active code context, change font.
+        cc.text['font'] = test_font
+        cc.font_timer_event()
+        eq(cc.textfont, test_font)
+        eq(cc.label['font'], test_font)
+
+        cc.text['font'] = save_font
+        cc.font_timer_event()
+
+
+class HelperFunctionText(unittest.TestCase):
+
+    def test_getspacesfirstword(self):
+        get = codecontext.getspacesfirstword
+        test_lines = (
+            ('    first word', ('    ', 'first')),
+            ('\tfirst word', ('\t', 'first')),
+            ('  \u19D4\u19D2: ', ('  ', '\u19D4\u19D2')),
+            ('no spaces', ('', 'no')),
+            ('', ('', '')),
+            ('# TEST COMMENT', ('', '')),
+            ('    (continuation)', ('    ', ''))
+            )
+        for line, expected_output in test_lines:
+            self.assertEqual(get(line), expected_output)
+
+        # Send the pattern in the call.
+        self.assertEqual(get('    (continuation)',
+                             c=re.compile(r'^(\s*)([^\s]*)')),
+                         ('    ', '(continuation)'))
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/Misc/NEWS.d/next/IDLE/2018-02-12-08-08-45.bpo-32831.srDRvU.rst b/Misc/NEWS.d/next/IDLE/2018-02-12-08-08-45.bpo-32831.srDRvU.rst
new file mode 100644
index 000000000000..583e341f94f0
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2018-02-12-08-08-45.bpo-32831.srDRvU.rst
@@ -0,0 +1 @@
+Add docstrings and tests for codecontext.



More information about the Python-checkins mailing list