(root)/
Python-3.12.0/
Lib/
idlelib/
idle_test/
test_codecontext.py
       1  "Test codecontext, coverage 100%"
       2  
       3  from idlelib import codecontext
       4  import unittest
       5  import unittest.mock
       6  from test.support import requires
       7  from tkinter import NSEW, Tk, Frame, Text, TclError
       8  
       9  from unittest import mock
      10  import re
      11  from idlelib import config
      12  
      13  
      14  usercfg = codecontext.idleConf.userCfg
      15  testcfg = {
      16      'main': config.IdleUserConfParser(''),
      17      'highlight': config.IdleUserConfParser(''),
      18      'keys': config.IdleUserConfParser(''),
      19      'extensions': config.IdleUserConfParser(''),
      20  }
      21  code_sample = """\
      22  
      23  class C1:
      24      # Class comment.
      25      def __init__(self, a, b):
      26          self.a = a
      27          self.b = b
      28      def compare(self):
      29          if a > b:
      30              return a
      31          elif a < b:
      32              return b
      33          else:
      34              return None
      35  """
      36  
      37  
      38  class ESC[4;38;5;81mDummyEditwin:
      39      def __init__(self, root, frame, text):
      40          self.root = root
      41          self.top = root
      42          self.text_frame = frame
      43          self.text = text
      44          self.label = ''
      45  
      46      def getlineno(self, index):
      47          return int(float(self.text.index(index)))
      48  
      49      def update_menu_label(self, **kwargs):
      50          self.label = kwargs['label']
      51  
      52  
      53  class ESC[4;38;5;81mCodeContextTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      54  
      55      @classmethod
      56      def setUpClass(cls):
      57          requires('gui')
      58          root = cls.root = Tk()
      59          root.withdraw()
      60          frame = cls.frame = Frame(root)
      61          text = cls.text = Text(frame)
      62          text.insert('1.0', code_sample)
      63          # Need to pack for creation of code context text widget.
      64          frame.pack(side='left', fill='both', expand=1)
      65          text.grid(row=1, column=1, sticky=NSEW)
      66          cls.editor = DummyEditwin(root, frame, text)
      67          codecontext.idleConf.userCfg = testcfg
      68  
      69      @classmethod
      70      def tearDownClass(cls):
      71          codecontext.idleConf.userCfg = usercfg
      72          cls.editor.text.delete('1.0', 'end')
      73          del cls.editor, cls.frame, cls.text
      74          cls.root.update_idletasks()
      75          cls.root.destroy()
      76          del cls.root
      77  
      78      def setUp(self):
      79          self.text.yview(0)
      80          self.text['font'] = 'TkFixedFont'
      81          self.cc = codecontext.CodeContext(self.editor)
      82  
      83          self.highlight_cfg = {"background": '#abcdef',
      84                                "foreground": '#123456'}
      85          orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight
      86          def mock_idleconf_GetHighlight(theme, element):
      87              if element == 'context':
      88                  return self.highlight_cfg
      89              return orig_idleConf_GetHighlight(theme, element)
      90          GetHighlight_patcher = unittest.mock.patch.object(
      91              codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
      92          GetHighlight_patcher.start()
      93          self.addCleanup(GetHighlight_patcher.stop)
      94  
      95          self.font_override = 'TkFixedFont'
      96          def mock_idleconf_GetFont(root, configType, section):
      97              return self.font_override
      98          GetFont_patcher = unittest.mock.patch.object(
      99              codecontext.idleConf, 'GetFont', mock_idleconf_GetFont)
     100          GetFont_patcher.start()
     101          self.addCleanup(GetFont_patcher.stop)
     102  
     103      def tearDown(self):
     104          if self.cc.context:
     105              self.cc.context.destroy()
     106          # Explicitly call __del__ to remove scheduled scripts.
     107          self.cc.__del__()
     108          del self.cc.context, self.cc
     109  
     110      def test_init(self):
     111          eq = self.assertEqual
     112          ed = self.editor
     113          cc = self.cc
     114  
     115          eq(cc.editwin, ed)
     116          eq(cc.text, ed.text)
     117          eq(cc.text['font'], ed.text['font'])
     118          self.assertIsNone(cc.context)
     119          eq(cc.info, [(0, -1, '', False)])
     120          eq(cc.topvisible, 1)
     121          self.assertIsNone(self.cc.t1)
     122  
     123      def test_del(self):
     124          self.cc.__del__()
     125  
     126      def test_del_with_timer(self):
     127          timer = self.cc.t1 = self.text.after(10000, lambda: None)
     128          self.cc.__del__()
     129          with self.assertRaises(TclError) as cm:
     130              self.root.tk.call('after', 'info', timer)
     131          self.assertIn("doesn't exist", str(cm.exception))
     132  
     133      def test_reload(self):
     134          codecontext.CodeContext.reload()
     135          self.assertEqual(self.cc.context_depth, 15)
     136  
     137      def test_toggle_code_context_event(self):
     138          eq = self.assertEqual
     139          cc = self.cc
     140          toggle = cc.toggle_code_context_event
     141  
     142          # Make sure code context is off.
     143          if cc.context:
     144              toggle()
     145  
     146          # Toggle on.
     147          toggle()
     148          self.assertIsNotNone(cc.context)
     149          eq(cc.context['font'], self.text['font'])
     150          eq(cc.context['fg'], self.highlight_cfg['foreground'])
     151          eq(cc.context['bg'], self.highlight_cfg['background'])
     152          eq(cc.context.get('1.0', 'end-1c'), '')
     153          eq(cc.editwin.label, 'Hide Code Context')
     154          eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
     155  
     156          # Toggle off.
     157          toggle()
     158          self.assertIsNone(cc.context)
     159          eq(cc.editwin.label, 'Show Code Context')
     160          self.assertIsNone(self.cc.t1)
     161  
     162          # Scroll down and toggle back on.
     163          line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0])
     164          cc.text.yview(11)
     165          toggle()
     166          eq(cc.context.get('1.0', 'end-1c'), line11_context)
     167  
     168          # Toggle off and on again.
     169          toggle()
     170          toggle()
     171          eq(cc.context.get('1.0', 'end-1c'), line11_context)
     172  
     173      def test_get_context(self):
     174          eq = self.assertEqual
     175          gc = self.cc.get_context
     176  
     177          # stopline must be greater than 0.
     178          with self.assertRaises(AssertionError):
     179              gc(1, stopline=0)
     180  
     181          eq(gc(3), ([(2, 0, 'class C1:', 'class')], 0))
     182  
     183          # Don't return comment.
     184          eq(gc(4), ([(2, 0, 'class C1:', 'class')], 0))
     185  
     186          # Two indentation levels and no comment.
     187          eq(gc(5), ([(2, 0, 'class C1:', 'class'),
     188                      (4, 4, '    def __init__(self, a, b):', 'def')], 0))
     189  
     190          # Only one 'def' is returned, not both at the same indent level.
     191          eq(gc(10), ([(2, 0, 'class C1:', 'class'),
     192                       (7, 4, '    def compare(self):', 'def'),
     193                       (8, 8, '        if a > b:', 'if')], 0))
     194  
     195          # With 'elif', also show the 'if' even though it's at the same level.
     196          eq(gc(11), ([(2, 0, 'class C1:', 'class'),
     197                       (7, 4, '    def compare(self):', 'def'),
     198                       (8, 8, '        if a > b:', 'if'),
     199                       (10, 8, '        elif a < b:', 'elif')], 0))
     200  
     201          # Set stop_line to not go back to first line in source code.
     202          # Return includes stop_line.
     203          eq(gc(11, stopline=2), ([(2, 0, 'class C1:', 'class'),
     204                                   (7, 4, '    def compare(self):', 'def'),
     205                                   (8, 8, '        if a > b:', 'if'),
     206                                   (10, 8, '        elif a < b:', 'elif')], 0))
     207          eq(gc(11, stopline=3), ([(7, 4, '    def compare(self):', 'def'),
     208                                   (8, 8, '        if a > b:', 'if'),
     209                                   (10, 8, '        elif a < b:', 'elif')], 4))
     210          eq(gc(11, stopline=8), ([(8, 8, '        if a > b:', 'if'),
     211                                   (10, 8, '        elif a < b:', 'elif')], 8))
     212  
     213          # Set stop_indent to test indent level to stop at.
     214          eq(gc(11, stopindent=4), ([(7, 4, '    def compare(self):', 'def'),
     215                                     (8, 8, '        if a > b:', 'if'),
     216                                     (10, 8, '        elif a < b:', 'elif')], 4))
     217          # Check that the 'if' is included.
     218          eq(gc(11, stopindent=8), ([(8, 8, '        if a > b:', 'if'),
     219                                     (10, 8, '        elif a < b:', 'elif')], 8))
     220  
     221      def test_update_code_context(self):
     222          eq = self.assertEqual
     223          cc = self.cc
     224          # Ensure code context is active.
     225          if not cc.context:
     226              cc.toggle_code_context_event()
     227  
     228          # Invoke update_code_context without scrolling - nothing happens.
     229          self.assertIsNone(cc.update_code_context())
     230          eq(cc.info, [(0, -1, '', False)])
     231          eq(cc.topvisible, 1)
     232  
     233          # Scroll down to line 1.
     234          cc.text.yview(1)
     235          cc.update_code_context()
     236          eq(cc.info, [(0, -1, '', False)])
     237          eq(cc.topvisible, 2)
     238          eq(cc.context.get('1.0', 'end-1c'), '')
     239  
     240          # Scroll down to line 2.
     241          cc.text.yview(2)
     242          cc.update_code_context()
     243          eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')])
     244          eq(cc.topvisible, 3)
     245          eq(cc.context.get('1.0', 'end-1c'), 'class C1:')
     246  
     247          # Scroll down to line 3.  Since it's a comment, nothing changes.
     248          cc.text.yview(3)
     249          cc.update_code_context()
     250          eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')])
     251          eq(cc.topvisible, 4)
     252          eq(cc.context.get('1.0', 'end-1c'), 'class C1:')
     253  
     254          # Scroll down to line 4.
     255          cc.text.yview(4)
     256          cc.update_code_context()
     257          eq(cc.info, [(0, -1, '', False),
     258                       (2, 0, 'class C1:', 'class'),
     259                       (4, 4, '    def __init__(self, a, b):', 'def')])
     260          eq(cc.topvisible, 5)
     261          eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
     262                                              '    def __init__(self, a, b):')
     263  
     264          # Scroll down to line 11.  Last 'def' is removed.
     265          cc.text.yview(11)
     266          cc.update_code_context()
     267          eq(cc.info, [(0, -1, '', False),
     268                       (2, 0, 'class C1:', 'class'),
     269                       (7, 4, '    def compare(self):', 'def'),
     270                       (8, 8, '        if a > b:', 'if'),
     271                       (10, 8, '        elif a < b:', 'elif')])
     272          eq(cc.topvisible, 12)
     273          eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
     274                                              '    def compare(self):\n'
     275                                              '        if a > b:\n'
     276                                              '        elif a < b:')
     277  
     278          # No scroll.  No update, even though context_depth changed.
     279          cc.update_code_context()
     280          cc.context_depth = 1
     281          eq(cc.info, [(0, -1, '', False),
     282                       (2, 0, 'class C1:', 'class'),
     283                       (7, 4, '    def compare(self):', 'def'),
     284                       (8, 8, '        if a > b:', 'if'),
     285                       (10, 8, '        elif a < b:', 'elif')])
     286          eq(cc.topvisible, 12)
     287          eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
     288                                              '    def compare(self):\n'
     289                                              '        if a > b:\n'
     290                                              '        elif a < b:')
     291  
     292          # Scroll up.
     293          cc.text.yview(5)
     294          cc.update_code_context()
     295          eq(cc.info, [(0, -1, '', False),
     296                       (2, 0, 'class C1:', 'class'),
     297                       (4, 4, '    def __init__(self, a, b):', 'def')])
     298          eq(cc.topvisible, 6)
     299          # context_depth is 1.
     300          eq(cc.context.get('1.0', 'end-1c'), '    def __init__(self, a, b):')
     301  
     302      def test_jumptoline(self):
     303          eq = self.assertEqual
     304          cc = self.cc
     305          jump = cc.jumptoline
     306  
     307          if not cc.context:
     308              cc.toggle_code_context_event()
     309  
     310          # Empty context.
     311          cc.text.yview('2.0')
     312          cc.update_code_context()
     313          eq(cc.topvisible, 2)
     314          cc.context.mark_set('insert', '1.5')
     315          jump()
     316          eq(cc.topvisible, 1)
     317  
     318          # 4 lines of context showing.
     319          cc.text.yview('12.0')
     320          cc.update_code_context()
     321          eq(cc.topvisible, 12)
     322          cc.context.mark_set('insert', '3.0')
     323          jump()
     324          eq(cc.topvisible, 8)
     325  
     326          # More context lines than limit.
     327          cc.context_depth = 2
     328          cc.text.yview('12.0')
     329          cc.update_code_context()
     330          eq(cc.topvisible, 12)
     331          cc.context.mark_set('insert', '1.0')
     332          jump()
     333          eq(cc.topvisible, 8)
     334  
     335          # Context selection stops jump.
     336          cc.text.yview('5.0')
     337          cc.update_code_context()
     338          cc.context.tag_add('sel', '1.0', '2.0')
     339          cc.context.mark_set('insert', '1.0')
     340          jump()  # Without selection, to line 2.
     341          eq(cc.topvisible, 5)
     342  
     343      @mock.patch.object(codecontext.CodeContext, 'update_code_context')
     344      def test_timer_event(self, mock_update):
     345          # Ensure code context is not active.
     346          if self.cc.context:
     347              self.cc.toggle_code_context_event()
     348          self.cc.timer_event()
     349          mock_update.assert_not_called()
     350  
     351          # Activate code context.
     352          self.cc.toggle_code_context_event()
     353          self.cc.timer_event()
     354          mock_update.assert_called()
     355  
     356      def test_font(self):
     357          eq = self.assertEqual
     358          cc = self.cc
     359  
     360          orig_font = cc.text['font']
     361          test_font = 'TkTextFont'
     362          self.assertNotEqual(orig_font, test_font)
     363  
     364          # Ensure code context is not active.
     365          if cc.context is not None:
     366              cc.toggle_code_context_event()
     367  
     368          self.font_override = test_font
     369          # Nothing breaks or changes with inactive code context.
     370          cc.update_font()
     371  
     372          # Activate code context, previous font change is immediately effective.
     373          cc.toggle_code_context_event()
     374          eq(cc.context['font'], test_font)
     375  
     376          # Call the font update, change is picked up.
     377          self.font_override = orig_font
     378          cc.update_font()
     379          eq(cc.context['font'], orig_font)
     380  
     381      def test_highlight_colors(self):
     382          eq = self.assertEqual
     383          cc = self.cc
     384  
     385          orig_colors = dict(self.highlight_cfg)
     386          test_colors = {'background': '#222222', 'foreground': '#ffff00'}
     387  
     388          def assert_colors_are_equal(colors):
     389              eq(cc.context['background'], colors['background'])
     390              eq(cc.context['foreground'], colors['foreground'])
     391  
     392          # Ensure code context is not active.
     393          if cc.context:
     394              cc.toggle_code_context_event()
     395  
     396          self.highlight_cfg = test_colors
     397          # Nothing breaks with inactive code context.
     398          cc.update_highlight_colors()
     399  
     400          # Activate code context, previous colors change is immediately effective.
     401          cc.toggle_code_context_event()
     402          assert_colors_are_equal(test_colors)
     403  
     404          # Call colors update with no change to the configured colors.
     405          cc.update_highlight_colors()
     406          assert_colors_are_equal(test_colors)
     407  
     408          # Call the colors update with code context active, change is picked up.
     409          self.highlight_cfg = orig_colors
     410          cc.update_highlight_colors()
     411          assert_colors_are_equal(orig_colors)
     412  
     413  
     414  class ESC[4;38;5;81mHelperFunctionText(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     415  
     416      def test_get_spaces_firstword(self):
     417          get = codecontext.get_spaces_firstword
     418          test_lines = (
     419              ('    first word', ('    ', 'first')),
     420              ('\tfirst word', ('\t', 'first')),
     421              ('  \u19D4\u19D2: ', ('  ', '\u19D4\u19D2')),
     422              ('no spaces', ('', 'no')),
     423              ('', ('', '')),
     424              ('# TEST COMMENT', ('', '')),
     425              ('    (continuation)', ('    ', ''))
     426              )
     427          for line, expected_output in test_lines:
     428              self.assertEqual(get(line), expected_output)
     429  
     430          # Send the pattern in the call.
     431          self.assertEqual(get('    (continuation)',
     432                               c=re.compile(r'^(\s*)([^\s]*)')),
     433                           ('    ', '(continuation)'))
     434  
     435      def test_get_line_info(self):
     436          eq = self.assertEqual
     437          gli = codecontext.get_line_info
     438          lines = code_sample.splitlines()
     439  
     440          # Line 1 is not a BLOCKOPENER.
     441          eq(gli(lines[0]), (codecontext.INFINITY, '', False))
     442          # Line 2 is a BLOCKOPENER without an indent.
     443          eq(gli(lines[1]), (0, 'class C1:', 'class'))
     444          # Line 3 is not a BLOCKOPENER and does not return the indent level.
     445          eq(gli(lines[2]), (codecontext.INFINITY, '    # Class comment.', False))
     446          # Line 4 is a BLOCKOPENER and is indented.
     447          eq(gli(lines[3]), (4, '    def __init__(self, a, b):', 'def'))
     448          # Line 8 is a different BLOCKOPENER and is indented.
     449          eq(gli(lines[7]), (8, '        if a > b:', 'if'))
     450          # Test tab.
     451          eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if'))
     452  
     453  
     454  if __name__ == '__main__':
     455      unittest.main(verbosity=2)