8000 bpo-21261: IDLE: Add autocomplete when completing dictionary keys by louisom · Pull Request #1511 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-21261: IDLE: Add autocomplete when completing dictionary keys #1511

New is 8000 sue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions Lib/idlelib/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Lib/idlelib/hyperparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
68 changes: 66 additions & 2 deletions Lib/idlelib/idle_test/test_autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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__':
Expand Down
29 changes: 28 additions & 1 deletion Lib/idlelib/idle_test/test_hyperparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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_st 4B6A ring_key())

def test_get_surrounding_bracket(self):
get = self.get_parser

Expand Down
0