diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/autocomplete.py index 1e44fa5bc66e8a..450f040bbc280c 100644 --- a/Lib/idlelib/autocomplete.py +++ b/Lib/idlelib/autocomplete.py @@ -9,7 +9,7 @@ # These constants represent the two different types of completions. # They must be defined here so autocomple_w can import them. -COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1) +COMPLETE_ATTRIBUTES, COMPLETE_FILES, COMPLETE_DICTIONARY_STRING_KEY = range(1, 3+1) from idlelib import autocomplete_w from idlelib.config import idleConf @@ -119,7 +119,18 @@ def open_completions(self, evalfuncs, complete, userWantsWin, mode=None): hp = HyperParser(self.editwin, "insert") curline = self.text.get("insert linestart", "insert") i = j = len(curline) - if hp.is_in_string() and (not mode or mode==COMPLETE_FILES): + if hp.is_in_subscript_string_key() and (not mode or mode==COMPLETE_DICTIONARY_STRING_KEY): + self._remove_autocomplete_window() + mode = COMPLETE_DICTIONARY_STRING_KEY + while i and (curline[i-1] not in '\'"'): + i -= 1 + comp_start = curline[i:j] + if i > 1: + hp.set_index('insert-%dc' % (len(curline) - (i - 2))) + comp_what = hp.get_expression() + else: + comp_what = "" + elif hp.is_in_string() and (not mode or mode==COMPLETE_FILES): # Find the beginning of the string # fetch_completions will look at the file system to determine whether the # string value constitutes an actual file name @@ -183,6 +194,7 @@ def fetch_completions(self, what, mode): return rpcclt.remotecall("exec", "get_the_completion_list", (what, mode), {}) else: + bigl = smalll = [] if mode == COMPLETE_ATTRIBUTES: if what == "": namespace = __main__.__dict__.copy() @@ -216,6 +228,17 @@ def fetch_completions(self, what, mode): except OSError: return [], [] + elif mode == COMPLETE_DICTIONARY_STRING_KEY: + try: + entity = self.get_entity(what) + + # Check the entity is dict + if not isinstance(entity, dict): + return [], [] + bigl = [s for s in entity if isinstance(s, str)] + bigl.sort() + except: + return [], [] if not smalll: smalll = bigl return smalll, bigl diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/hyperparser.py index 450a709c09bbfa..4c533a550c9299 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/hyperparser.py @@ -113,6 +113,12 @@ def is_in_code(self): self.rawtext[self.bracketing[self.indexbracket][0]] not in ('#', '"', "'")) + def is_in_subscript_string_key(self): + """Is the index given to the HyperParser in subscript with string key?""" + return (self.isopener[self.indexbracket] and + self.rawtext[self.bracketing[self.indexbracket - 1][0]] == '[' and + self.rawtext[self.bracketing[self.indexbracket][0]] in ('"', "'")) + def get_surrounding_brackets(self, openers='([{', mustclose=False): """Return bracket indexes or None. diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py index f3f2dea4246df0..8f1ec633ac04c1 100644 --- a/Lib/idlelib/idle_test/test_autocomplete.py +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -2,7 +2,9 @@ Coverage of autocomple: 56% ''' +import os import unittest +from unittest.mock import Mock, patch from test.support import requires from tkinter import Tk, Text @@ -137,12 +139,74 @@ def test_fetch_completions(self): # a small list containing non-private variables. # For file completion, a large list containing all files in the path, # and a small list containing files that do not start with '.' - pass + # For dictionary key completion, both list containing string keys in the + # given dictionary. + autocomplete = self.autocomplete + + # Test attributes + s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES) + self.assertTrue(all(filter(lambda x: x.startswith('_'), s))) + self.assertTrue(any(filter(lambda x: x.startswith('_'), b))) + + # Test smalll should respect to __all__ + with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}): + s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES) + self.assertEqual(s, ['a', 'b']) + self.assertIn('__name__', b) # From __main__.__dict__ + self.assertIn('sum', b) # From __main__.__builtins__.__dict__ + + # Test attributes with name entity + mock = Mock() + mock._private = Mock() + with patch.dict('__main__.__dict__', {'foo': mock}): + s, b = autocomplete.fetch_completions('foo', ac.COMPLETE_ATTRIBUTES) + self.assertNotIn('_private', s) + self.assertIn('_private', b) + self.assertEqual(s, [i for i in sorted(dir(mock)) if i[:1] != '_']) + self.assertEqual(b, sorted(dir(mock))) + + # Test files + def _listdir(path): + if path == '.': + return ['foo', 'bar', '.hidden'] + return ['monty', 'python', '.hidden'] + + with patch.object(os, 'listdir', _listdir): + s, b = autocomplete.fetch_completions('', ac.COMPLETE_FILES) + self.assertEqual(s, ['bar', 'foo']) + self.assertEqual(b, ['.hidden', 'bar', 'foo']) + + s, b = autocomplete.fetch_completions('~', ac.COMPLETE_FILES) + self.assertEqual(s, ['monty', 'python']) + self.assertEqual(b, ['.hidden', 'monty', 'python']) + + # Test dictionary string key + d = {'foo': 10, 'bar': 20, 30: 40} + with patch.dict('__main__.__dict__', {'foo': d}): + s, b = autocomplete.fetch_completions('foo', ac.COMPLETE_DICTIONARY_STRING_KEY) + self.assertEqual(s, b) + self.assertNotIn(30, s) def test_get_entity(self): # Test that a name is in the namespace of sys.modules and # __main__.__dict__ - pass + autocomplete = self.autocomplete + Equal = self.assertEqual + + # Test name from sys.modules + mock = Mock() + with patch.dict('sys.modules', {'tempfile': mock}): + Equal(autocomplete.get_entity('tempfile'), mock) + + # Test name from __main__.__dict__ + di = {'foo': 10, 'bar': 20} + with patch.dict('__main__.__dict__', {'d': di}): + Equal(autocomplete.get_entity('d'), di) + + # Test name not in namespace + with patch.dict('__main__.__dict__', {}): + with self.assertRaises(NameError): + autocomplete.get_entity('not_exist') if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py index 73c8281e430d64..5dc66ecd9ae862 100644 --- a/Lib/idlelib/idle_test/test_hyperparser.py +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -30,7 +30,10 @@ class HyperParserTest(unittest.TestCase): "z = ((r'asdf')+('a')))\n" '[x for x in\n' 'for = False\n' - 'cliché = "this is a string with unicode, what a cliché"' + 'cliché = "this is a string with unicode, what a cliché"\n' + "d['long_key']\n" + 'd["short"]\n' + 'd[key]' ) @classmethod @@ -114,6 +117,30 @@ def test_is_in_code(self): p = get('4.14') self.assertFalse(p.is_in_code()) + def test_is_in_subscript_string_key(self): + get = self.get_parser + + p = get('5.0') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('7.0') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('8.0') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('9.6') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('13.2') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('13.3') + self.assertTrue(p.is_in_subscript_string_key()) + p = get('13.15') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('14.2') + self.assertFalse(p.is_in_subscript_string_key()) + p = get('14.3') + self.assertTrue(p.is_in_subscript_string_key()) + p = get('15.2') + self.assertFalse(p.is_in_subscript_string_key()) + def test_get_surrounding_bracket(self): get = self.get_parser