Skip to content

Commit 82c3f1e

Browse files
importcompletion.complete rewrite, simple tests passing
The tests are terrible atm - they take a second to run each as they redo the search for modules for each. Need to do this artificially or worst case just once.
1 parent e14825a commit 82c3f1e

File tree

8 files changed

+324
-80
lines changed

8 files changed

+324
-80
lines changed

bpython/autocomplete.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@
2424
from __future__ import with_statement
2525
import __builtin__
2626
import rlcompleter
27+
import line
2728
import re
29+
import os
30+
from glob import glob
2831
from bpython import inspection
32+
from bpython import importcompletion
33+
from bpython._py3compat import py3
2934

3035
# Needed for special handling of __abstractmethods__
3136
# abc only exists since 2.6, so check both that it exists and that it's
@@ -44,6 +49,10 @@
4449

4550
class Autocomplete(rlcompleter.Completer):
4651
"""
52+
#TOMHERE TODO This doesn't need to be a class anymore - we're overriding every single method
53+
We're not really using it for the right thing anyway - we're hacking it to produce
54+
self.matches then stealing them instead of following the expected interface of calling
55+
complete each time.
4756
"""
4857

4958
def __init__(self, namespace = None, config = None):
@@ -176,3 +185,58 @@ def method_match(self, word, size, text):
176185
s = r'.*%s.*' % '.*'.join(list(text))
177186
return re.search(s, word)
178187

188+
def filename_matches(cs):
189+
matches = []
190+
username = cs.split(os.path.sep, 1)[0]
191+
user_dir = os.path.expanduser(username)
192+
for filename in glob(os.path.expanduser(cs + '*')):
193+
if os.path.isdir(filename):
194+
filename += os.path.sep
195+
if cs.startswith('~'):
196+
filename = username + filename[len(user_dir):]
197+
matches.append(filename)
198+
return cs
199+
200+
def find_matches(cursor_offset, current_line, locals_, current_string_callback, completer, magic_methods, argspec):
201+
"""Returns a list of matches and function to use for replacing words on tab"""
202+
203+
if line.current_string(cursor_offset, current_line):
204+
matches = filename_matches(line.current_string(cursor_offset, current_line))
205+
return matches, line.current_string
206+
207+
if line.current_word(cursor_offset, current_line) is None:
208+
return [], None
209+
210+
matches = importcompletion.complete(cursor_offset, current_line)
211+
return matches, line.current_word
212+
213+
cw = line.current_word(cursor_offset, current_line)[2]
214+
215+
try:
216+
completer.complete(cw, 0)
217+
#except Exception:
218+
# # This sucks, but it's either that or list all the exceptions that could
219+
# # possibly be raised here, so if anyone wants to do that, feel free to send me
220+
# # a patch. XXX: Make sure you raise here if you're debugging the completion
221+
# # stuff !
222+
# e = True
223+
#else:
224+
e = False
225+
matches = completer.matches
226+
matches.extend(magic_methods(cw))
227+
except KeyboardInterrupt:
228+
pass
229+
230+
if not e and argspec:
231+
matches.extend(name + '=' for name in argspec[1][0]
232+
if isinstance(name, basestring) and name.startswith(cw))
233+
if py3:
234+
matches.extend(name + '=' for name in argspec[1][4]
235+
if name.startswith(cw))
236+
237+
# unless the first character is a _ filter out all attributes starting with a _
238+
if not e and not cw.split('.')[-1].startswith('_'):
239+
matches = [match for match in matches
240+
if not match.split('.')[-1].startswith('_')]
241+
242+
return sorted(set(matches)), line.current_word

bpython/curtsiesfrontend/repl.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,11 @@ def only_whitespace_left_of_cursor():
395395
self.add_normal_character(' ')
396396
return
397397

398-
#TODO I'm not sure what's going on in the next 10 lines, particularly list_win_visible
399398
# get the (manually typed or common-sequence completed from manually typed) current word
400399
if self.matches_iter:
401400
cw = self.matches_iter.current_word
402401
else:
403-
self.complete(tab=True) #TODO why do we call this here?
402+
self.complete(tab=True)
404403
if not self.config.auto_display_list and not self.list_win_visible:
405404
return True #TODO why?
406405
cw = self.current_string() or self.current_word
@@ -417,6 +416,8 @@ def only_whitespace_left_of_cursor():
417416
self.matches_iter.update(cseq, self.matches)
418417
return
419418

419+
#TODO save how to do the replacement as a function on self.matches
420+
# so it can be used here
420421
if self.matches:
421422
self.current_word = (self.matches_iter.previous()
422423
if back else self.matches_iter.next())

bpython/importcompletion.py

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from __future__ import with_statement
2424

25+
from bpython import line as lineparts
2526
import imp
2627
import os
2728
import sys
@@ -48,65 +49,69 @@ def catch_warnings():
4849
fully_loaded = False
4950

5051

51-
def complete(line, cw):
52+
def complete(cursor_offset, line):
5253
"""Construct a full list of possibly completions for imports."""
53-
if not cw:
54-
return None
55-
5654
# TODO if this is done in a thread (as it prob will be in Windows) we'll need this
5755
# if not fully_loaded:
5856
# return []
59-
6057
tokens = line.split()
61-
if tokens[0] not in ['from', 'import']:
58+
if 'from' not in tokens and 'import' not in tokens:
6259
return None
6360

64-
completing_from = False
65-
if tokens[0] == 'from':
66-
if len(tokens) > 3:
67-
if '.' in cw:
68-
# This will result in a SyntaxError, so do not return
69-
# any matches
70-
return None
71-
completing_from = True
72-
cw = '%s.%s' % (tokens[1], cw)
73-
elif len(tokens) == 3:
74-
if 'import '.startswith(cw):
75-
return ['import ']
76-
else:
77-
# Will result in a SyntaxError
78-
return None
79-
80-
matches = list()
81-
for name in modules:
82-
if not (name.startswith(cw) and name.find('.', len(cw)) == -1):
83-
continue
84-
if completing_from:
85-
name = name[len(tokens[1]) + 1:]
86-
matches.append(name)
87-
if completing_from and tokens[1] in sys.modules:
88-
# from x import y -> search for attributes starting with y if
89-
# x is in sys.modules
90-
_, _, cw = cw.rpartition('.')
91-
module = sys.modules[tokens[1]]
92-
matches.extend(name for name in dir(module) if name.startswith(cw))
93-
elif len(tokens) == 2:
94-
# from x.y or import x.y -> search for attributes starting
95-
# with y if x is in sys.modules and the attribute is also in
96-
# sys.modules
97-
module_name, _, cw = cw.rpartition('.')
98-
if module_name in sys.modules:
99-
module = sys.modules[module_name]
100-
for name in dir(module):
101-
if not name.startswith(cw):
102-
continue
103-
submodule_name = '%s.%s' % (module_name, name)
104-
if submodule_name in sys.modules:
105-
matches.append(submodule_name)
106-
if not matches:
107-
return []
108-
return matches
61+
result = lineparts.current_word(cursor_offset, line)
62+
if result is None:
63+
return None
10964

65+
def module_matches(cw, prefix=''):
66+
"""Modules names to replace cw with"""
67+
full = '%s.%s' % (prefix, cw) if prefix else cw
68+
matches = [name for name in modules
69+
if (name.startswith(full) and
70+
name.find('.', len(full)) == -1)]
71+
if prefix:
72+
return [match[len(prefix)+1:] for match in matches]
73+
else:
74+
return matches
75+
76+
def attr_matches(cw, prefix='', only_modules=False):
77+
"""Attributes to replace name with"""
78+
full = '%s.%s' % (prefix, cw) if prefix else cw
79+
module_name, _, name_after_dot = full.rpartition('.')
80+
if module_name not in sys.modules:
81+
return []
82+
module = sys.modules[module_name]
83+
if only_modules:
84+
matches = [name for name in dir(module)
85+
if name.startswith(name_after_dot) and
86+
'%s.%s' % (module_name, name) in sys.modules]
87+
else:
88+
matches = [name for name in dir(module) if name.startswith(name_after_dot)]
89+
module_part, _, _ = cw.rpartition('.')
90+
if module_part:
91+
return ['%s.%s' % (module_part, m) for m in matches]
92+
return matches
93+
94+
def module_attr_matches(name):
95+
"""Only attributes which are modules to replace name with"""
96+
return attr_matches(name, prefix='', only_modules=True)
97+
98+
if lineparts.current_from_import_from(cursor_offset, line) is not None:
99+
if lineparts.current_from_import_import(cursor_offset, line) is not None:
100+
# `from a import <b|>` completion
101+
return (module_matches(lineparts.current_from_import_import(cursor_offset, line)[2],
102+
lineparts.current_from_import_from(cursor_offset, line)[2]) +
103+
attr_matches(lineparts.current_from_import_import(cursor_offset, line)[2],
104+
lineparts.current_from_import_from(cursor_offset, line)[2]))
105+
else:
106+
# `from <a|>` completion
107+
return (module_attr_matches(lineparts.current_from_import_from(cursor_offset, line)[2]) +
108+
module_matches(lineparts.current_from_import_from(cursor_offset, line)[2]))
109+
elif lineparts.current_import(cursor_offset, line):
110+
# `import <a|>` completion
111+
return (module_matches(lineparts.current_import(cursor_offset, line)[2]) +
112+
module_attr_matches(lineparts.current_import(cursor_offset, line)[2]))
113+
else:
114+
return None
110115

111116
def find_modules(path):
112117
"""Find all modules (and packages) for a given directory."""
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,54 @@ def current_object_attribute(cursor_offset, line):
7474
if m.start(1) + start <= cursor_offset and m.end(1) + start >= cursor_offset:
7575
return m.start(1) + start, m.end(1) + start, m.group(1)
7676
return None
77+
78+
def current_from_import_from(cursor_offset, line):
79+
"""If in from import completion, the word after from
80+
81+
returns None if cursor not in or just after one of the two interesting parts
82+
of an import: from (module) import (name1, name2)
83+
"""
84+
#TODO allow for as's
85+
tokens = line.split()
86+
if not ('from' in tokens or 'import' in tokens):
87+
return None
88+
matches = list(re.finditer(r'from ([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*', line))
89+
for m in matches:
90+
if ((m.start(1) < cursor_offset and m.end(1) >= cursor_offset) or
91+
(m.start(2) < cursor_offset and m.end(2) >= cursor_offset)):
92+
return m.start(1), m.end(1), m.group(1)
93+
return None
94+
95+
def current_from_import_import(cursor_offset, line):
96+
"""If in from import completion, the word after import being completed
97+
98+
returns None if cursor not in or just after one of these words
99+
"""
100+
baseline = re.search(r'from\s([\w0-9_.]*)\s+import', line)
101+
if baseline is None:
102+
return None
103+
match1 = re.search(r'([\w0-9_]+)', line[baseline.end():])
104+
if match1 is None:
105+
return None
106+
matches = list(re.finditer(r'[,][ ]([\w0-9_]*)', line[baseline.end():]))
107+
for m in [match1] + matches:
108+
start = baseline.end() + m.start(1)
109+
end = baseline.end() + m.end(1)
110+
if start < cursor_offset and end >= cursor_offset:
111+
return start, end, m.group(1)
112+
return None
113+
114+
def current_import(cursor_offset, line):
115+
#TODO allow for multiple as's
116+
baseline = re.search(r'import', line)
117+
if baseline is None:
118+
return None
119+
match1 = re.search(r'([\w0-9_.]+)', line[baseline.end():])
120+
if match1 is None:
121+
return None
122+
matches = list(re.finditer(r'[,][ ]([\w0-9_.]*)', line[baseline.end():]))
123+
for m in [match1] + matches:
124+
start = baseline.end() + m.start(1)
125+
end = baseline.end() + m.end(1)
126+
if start < cursor_offset and end >= cursor_offset:
127+
return start, end, m.group(1)

bpython/repl.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -550,12 +550,7 @@ def get_source_of_current_name(self):
550550
else:
551551
return source
552552

553-
def complete(self, tab=False):
554-
"""Construct a full list of possible completions and construct and
555-
display them in a window. Also check if there's an available argspec
556-
(via the inspect module) and bang that on top of the completions too.
557-
The return value is whether the list_win is visible or not."""
558-
553+
def set_docstring(self):
559554
self.docstring = None
560555
if not self.get_args():
561556
self.argspec = None
@@ -570,6 +565,28 @@ def complete(self, tab=False):
570565
if not self.docstring:
571566
self.docstring = None
572567

568+
def magic_method_completions(self, cw):
569+
if (self.config.complete_magic_methods and self.buffer and
570+
self.buffer[0].startswith("class ") and
571+
self.current_line().lstrip().startswith("def ")):
572+
return [name for name in self.config.magic_methods
573+
if name.startswith(cw)]
574+
else:
575+
return []
576+
577+
def complete(self, tab=False):
578+
"""Construct a full list of possible completions and construct and
579+
display them in a window. Also check if there's an available argspec
580+
(via the inspect module) and bang that on top of the completions too.
581+
The return value is whether the list_win is visible or not."""
582+
583+
#TODO keep factoring out parts of complete
584+
# so we can reuse code when we reimplement complete
585+
# in bpython.curtsiesfrontend.repl
586+
# Maybe move things to autocomplete instead of here?
587+
588+
self.set_docstring()
589+
573590
cw = self.cw()
574591
cs = self.current_string()
575592
if not cw:
@@ -579,16 +596,7 @@ def complete(self, tab=False):
579596
return bool(self.argspec)
580597

581598
if cs and tab:
582-
# Filename completion
583-
self.matches = list()
584-
username = cs.split(os.path.sep, 1)[0]
585-
user_dir = os.path.expanduser(username)
586-
for filename in glob(os.path.expanduser(cs + '*')):
587-
if os.path.isdir(filename):
588-
filename += os.path.sep
589-
if cs.startswith('~'):
590-
filename = username + filename[len(user_dir):]
591-
self.matches.append(filename)
599+
self.matches = filename_matches(cs)
592600
self.matches_iter.update(cs, self.matches)
593601
return bool(self.matches)
594602
elif cs:
@@ -599,7 +607,7 @@ def complete(self, tab=False):
599607

600608
# Check for import completion
601609
e = False
602-
matches = importcompletion.complete(self.current_line(), cw)
610+
matches = importcompletion.complete(len(self.current_line()) - self.cpos, self.current_line())
603611
if matches is not None and not matches:
604612
self.matches = []
605613
self.matches_iter.update()
@@ -617,11 +625,7 @@ def complete(self, tab=False):
617625
e = True
618626
else:
619627
matches = self.completer.matches
620-
if (self.config.complete_magic_methods and self.buffer and
621-
self.buffer[0].startswith("class ") and
622-
self.current_line().lstrip().startswith("def ")):
623-
matches.extend(name for name in self.config.magic_methods
624-
if name.startswith(cw))
628+
matches.extend(self.magic_method_completions(cw))
625629

626630
if not e and self.argspec:
627631
matches.extend(name + '=' for name in self.argspec[1][0]
@@ -1091,3 +1095,14 @@ def extract_exit_value(args):
10911095
else:
10921096
return args
10931097

1098+
def filename_matches(cs):
1099+
matches = []
1100+
username = cs.split(os.path.sep, 1)[0]
1101+
user_dir = os.path.expanduser(username)
1102+
for filename in glob(os.path.expanduser(cs + '*')):
1103+
if os.path.isdir(filename):
1104+
filename += os.path.sep
1105+
if cs.startswith('~'):
1106+
filename = username + filename[len(user_dir):]
1107+
matches.append(filename)
1108+
return cs

0 commit comments

Comments
 (0)