From de42b33528c91e379f00716871eb5926915c41cc Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Mon, 8 May 2017 19:22:44 +0800 Subject: [PATCH 1/3] bpo-21261: Add autocomplete to IDLE when completing dictionary keys --- Lib/idlelib/autocomplete.py | 29 +++++++++++++++++++++-- Lib/idlelib/hyperparser.py | 10 ++++++++ Lib/idlelib/idle_test/test_hyperparser.py | 29 ++++++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/autocomplete.py index 1e44fa5bc66e8a..1914ef3a04b33e 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_KEY = range(1, 3+1) from idlelib import autocomplete_w from idlelib.config import idleConf @@ -119,7 +119,20 @@ 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() and (not mode or mode==COMPLETE_DICTIONARY_KEY): + self._remove_autocomplete_window() + mode = COMPLETE_DICTIONARY_KEY + while i and (curline[i-1] not in '[\'"'): + i -= 1 + comp_start = curline[i:j] + if i > 1: + if curline[i - 1] == '[': + 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 +196,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 +230,17 @@ def fetch_completions(self, what, mode): except OSError: return [], [] + elif mode == COMPLETE_DICTIONARY_KEY: + try: + entity = self.get_entity(what) + + # Check the entity is dict + if not isinstance(entity, dict): + return [], [] + bigl = [str(s) for s in entity] + 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..9df993f4f0e9b2 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/hyperparser.py @@ -113,6 +113,16 @@ def is_in_code(self): self.rawtext[self.bracketing[self.indexbracket][0]] not in ('#', '"', "'")) + def is_in_subscript(self): + """Is the index given to the HyperParser in subscript?""" + if not self.isopener[self.indexbracket]: + return False + for index, bracket in enumerate(self.bracketing[: self.indexbracket + 1]): + if (self.isopener[index] is True and + self.rawtext[bracket[0]] == '['): + return True + return False + def get_surrounding_brackets(self, openers='([{', mustclose=False): """Return bracket indexes or None. diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py index 73c8281e430d64..e8f033ba05d1a6 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(self): + get = self.get_parser + + p = get('5.0') + self.assertFalse(p.is_in_subscript()) + p = get('7.0') + self.assertFalse(p.is_in_subscript()) + p = get('8.0') + self.assertFalse(p.is_in_subscript()) + p = get('9.6') + self.assertFalse(p.is_in_subscript()) + p = get('13.2') + self.assertTrue(p.is_in_subscript()) + p = get('13.3') + self.assertTrue(p.is_in_subscript()) + p = get('13.15') + self.assertFalse(p.is_in_subscript()) + p = get('14.2') + self.assertTrue(p.is_in_subscript()) + p = get('14.3') + self.assertTrue(p.is_in_subscript()) + p = get('15.2') + self.assertTrue(p.is_in_subscript()) + def test_get_surrounding_bracket(self): get = self.get_parser From 3c4a811a7897576a0d332522604abd3c5339b99b Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Wed, 10 May 2017 16:31:38 +0800 Subject: [PATCH 2/3] Update will only complete on string key --- Lib/idlelib/autocomplete.py | 14 ++++++-------- Lib/idlelib/hyperparser.py | 14 +++++--------- Lib/idlelib/idle_test/test_hyperparser.py | 22 +++++++++++----------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/autocomplete.py index 1914ef3a04b33e..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, COMPLETE_DICTIONARY_KEY = range(1, 3+1) +COMPLETE_ATTRIBUTES, COMPLETE_FILES, COMPLETE_DICTIONARY_STRING_KEY = range(1, 3+1) from idlelib import autocomplete_w from idlelib.config import idleConf @@ -119,15 +119,13 @@ 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_subscript() and (not mode or mode==COMPLETE_DICTIONARY_KEY): + if hp.is_in_subscript_string_key() and (not mode or mode==COMPLETE_DICTIONARY_STRING_KEY): self._remove_autocomplete_window() - mode = COMPLETE_DICTIONARY_KEY - while i and (curline[i-1] not in '[\'"'): + mode = COMPLETE_DICTIONARY_STRING_KEY + while i and (curline[i-1] not in '\'"'): i -= 1 comp_start = curline[i:j] if i > 1: - if curline[i - 1] == '[': - i += 1 hp.set_index('insert-%dc' % (len(curline) - (i - 2))) comp_what = hp.get_expression() else: @@ -230,14 +228,14 @@ def fetch_completions(self, what, mode): except OSError: return [], [] - elif mode == COMPLETE_DICTIONARY_KEY: + elif mode == COMPLETE_DICTIONARY_STRING_KEY: try: entity = self.get_entity(what) # Check the entity is dict if not isinstance(entity, dict): return [], [] - bigl = [str(s) for s in entity] + bigl = [s for s in entity if isinstance(s, str)] bigl.sort() except: return [], [] diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/hyperparser.py index 9df993f4f0e9b2..4c533a550c9299 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/hyperparser.py @@ -113,15 +113,11 @@ def is_in_code(self): self.rawtext[self.bracketing[self.indexbracket][0]] not in ('#', '"', "'")) - def is_in_subscript(self): - """Is the index given to the HyperParser in subscript?""" - if not self.isopener[self.indexbracket]: - return False - for index, bracket in enumerate(self.bracketing[: self.indexbracket + 1]): - if (self.isopener[index] is True and - self.rawtext[bracket[0]] == '['): - return True - return False + 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_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py index e8f033ba05d1a6..5dc66ecd9ae862 100644 --- a/Lib/idlelib/idle_test/test_hyperparser.py +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -117,29 +117,29 @@ def test_is_in_code(self): p = get('4.14') self.assertFalse(p.is_in_code()) - def test_is_in_subscript(self): + def test_is_in_subscript_string_key(self): get = self.get_parser p = get('5.0') - self.assertFalse(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('7.0') - self.assertFalse(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('8.0') - self.assertFalse(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('9.6') - self.assertFalse(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('13.2') - self.assertTrue(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('13.3') - self.assertTrue(p.is_in_subscript()) + self.assertTrue(p.is_in_subscript_string_key()) p = get('13.15') - self.assertFalse(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('14.2') - self.assertTrue(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) p = get('14.3') - self.assertTrue(p.is_in_subscript()) + self.assertTrue(p.is_in_subscript_string_key()) p = get('15.2') - self.assertTrue(p.is_in_subscript()) + self.assertFalse(p.is_in_subscript_string_key()) def test_get_surrounding_bracket(self): get = self.get_parser From 73c151273b9c85ad7d3754d37248f70763e598d9 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Fri, 12 May 2017 15:52:26 +0800 Subject: [PATCH 3/3] Add fetch completions and get entity unittest --- Lib/idlelib/idle_test/test_autocomplete.py | 68 +++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) 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__':