Skip to content

Commit aea1a25

Browse files
completion chooser abstracted, tested
also tests for cumulative completion and filename completion
1 parent 2d34994 commit aea1a25

File tree

3 files changed

+207
-82
lines changed

3 files changed

+207
-82
lines changed

bpython/autocomplete.py

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import re
3030
import os
3131
from glob import glob
32+
from functools import partial
3233
from bpython import inspection
3334
from bpython import importcompletion
3435
from bpython._py3compat import py3
@@ -50,7 +51,7 @@
5051

5152
MAGIC_METHODS = ["__%s__" % s for s in [
5253
"init", "repr", "str", "lt", "le", "eq", "ne", "gt", "ge", "cmp", "hash",
53-
"nonzero", "unicode", "getattr", "setattr", "get", "set","call", "len",
54+
"nonzero", "unicode", "getattr", "setattr", "get", "set", "call", "len",
5455
"getitem", "setitem", "iter", "reversed", "contains", "add", "sub", "mul",
5556
"floordiv", "mod", "divmod", "pow", "lshift", "rshift", "and", "xor", "or",
5657
"div", "truediv", "neg", "pos", "abs", "invert", "complex", "int", "float",
@@ -60,15 +61,14 @@
6061
def after_last_dot(name):
6162
return name.rstrip('.').rsplit('.')[-1]
6263

63-
def get_completer(cursor_offset, current_line, locals_, argspec, current_block,
64-
mode, complete_magic_methods):
65-
"""Returns a list of matches and a class for what kind of completion is happening
64+
def get_completer(completers, cursor_offset, line, **kwargs):
65+
"""Returns a list of matches and an applicable completer
6666
67-
If no completion type is relevant, returns None, None
67+
If no matches available, returns a tuple of an empty list and None
6868
69-
Params:
69+
kwargs (all required):
7070
cursor_offset is the current cursor column
71-
current_line is a string of the current line
71+
line is a string of the current line
7272
locals_ is a dictionary of the environment
7373
argspec is an inspect.ArgSpec instance for the current function where
7474
the cursor is
@@ -79,53 +79,42 @@ def get_completer(cursor_offset, current_line, locals_, argspec, current_block,
7979
double underscore methods like __len__ in method signatures
8080
"""
8181

82-
kwargs = {'locals_':locals_, 'argspec':argspec, 'current_block':current_block,
83-
'mode':mode, 'complete_magic_methods':complete_magic_methods}
84-
85-
# mutually exclusive if matches: If one of these returns [], try the next one
86-
for completer in [DictKeyCompletion]:
87-
matches = completer.matches(cursor_offset, current_line, **kwargs)
88-
if matches:
89-
return sorted(set(matches)), completer
90-
91-
# mutually exclusive matchers: if one returns [], don't go on
92-
for completer in [StringLiteralAttrCompletion, ImportCompletion,
93-
FilenameCompletion, MagicMethodCompletion, GlobalCompletion]:
94-
matches = completer.matches(cursor_offset, current_line, **kwargs)
82+
for completer in completers:
83+
matches = completer.matches(cursor_offset, line, **kwargs)
9584
if matches is not None:
96-
return sorted(set(matches)), completer
97-
98-
matches = AttrCompletion.matches(cursor_offset, current_line, **kwargs)
99-
100-
# cumulative completions - try them all
101-
# They all use current_word replacement and formatting
102-
current_word_matches = []
103-
for completer in [AttrCompletion, ParameterNameCompletion]:
104-
matches = completer.matches(cursor_offset, current_line, **kwargs)
105-
if matches is not None:
106-
current_word_matches.extend(matches)
107-
108-
if len(current_word_matches) == 0:
109-
return None, None
110-
return sorted(set(current_word_matches)), AttrCompletion
85+
return matches, (completer if matches else None)
86+
return [], None
87+
88+
def get_completer_bpython(**kwargs):
89+
""""""
90+
return get_completer([DictKeyCompletion,
91+
StringLiteralAttrCompletion,
92+
ImportCompletion,
93+
FilenameCompletion,
94+
MagicMethodCompletion,
95+
GlobalCompletion,
96+
CumulativeCompleter([AttrCompletion, ParameterNameCompletion])],
97+
**kwargs)
11198

11299
class BaseCompletionType(object):
113100
"""Describes different completion types"""
101+
@classmethod
114102
def matches(cls, cursor_offset, line, **kwargs):
115103
"""Returns a list of possible matches given a line and cursor, or None
116104
if this completion type isn't applicable.
117105
118106
ie, import completion doesn't make sense if there cursor isn't after
119-
an import or from statement
107+
an import or from statement, so it ought to return None.
120108
121109
Completion types are used to:
122-
* `locate(cur, line)` their target word to replace given a line and cursor
110+
* `locate(cur, line)` their initial target word to replace given a line and cursor
123111
* find `matches(cur, line)` that might replace that word
124112
* `format(match)` matches to be displayed to the user
125113
* determine whether suggestions should be `shown_before_tab`
126114
* `substitute(cur, line, match)` in a match for what's found with `target`
127115
"""
128116
raise NotImplementedError
117+
@classmethod
129118
def locate(cls, cursor_offset, line):
130119
"""Returns a start, stop, and word given a line and cursor, or None
131120
if no target for this type of completion is found under the cursor"""
@@ -134,25 +123,58 @@ def locate(cls, cursor_offset, line):
134123
def format(cls, word):
135124
return word
136125
shown_before_tab = True # whether suggestions should be shown before the
137-
# user hits tab, or only once that has happened
126+
# user hits tab, or only once that has happened
138127
def substitute(cls, cursor_offset, line, match):
139128
"""Returns a cursor offset and line with match swapped in"""
140129
start, end, word = cls.locate(cursor_offset, line)
141130
result = start + len(match), line[:start] + match + line[end:]
142131
return result
143132

133+
class CumulativeCompleter(object):
134+
"""Returns combined matches from several completers"""
135+
def __init__(self, completers):
136+
if not completers:
137+
raise ValueError("CumulativeCompleter requires at least one completer")
138+
self._completers = completers
139+
self.shown_before_tab = True
140+
141+
@property
142+
def locate(self):
143+
return self._completers[0].locate if self._completers else lambda *args: None
144+
145+
@property
146+
def format(self):
147+
return self._completers[0].format if self._completers else lambda s: s
148+
149+
def matches(self, cursor_offset, line, locals_, argspec, current_block, complete_magic_methods):
150+
all_matches = []
151+
for completer in self._completers:
152+
# these have to be explicitely listed to deal with the different
153+
# signatures of various matches() methods of completers
154+
matches = completer.matches(cursor_offset=cursor_offset,
155+
line=line,
156+
locals_=locals_,
157+
argspec=argspec,
158+
current_block=current_block,
159+
complete_magic_methods=complete_magic_methods)
160+
if matches is not None:
161+
all_matches.extend(matches)
162+
163+
return sorted(set(all_matches))
164+
165+
144166
class ImportCompletion(BaseCompletionType):
145167
@classmethod
146-
def matches(cls, cursor_offset, current_line, **kwargs):
147-
return importcompletion.complete(cursor_offset, current_line)
168+
def matches(cls, cursor_offset, line, **kwargs):
169+
return importcompletion.complete(cursor_offset, line)
148170
locate = staticmethod(lineparts.current_word)
149171
format = staticmethod(after_last_dot)
150172

151173
class FilenameCompletion(BaseCompletionType):
152174
shown_before_tab = False
153175
@classmethod
154-
def matches(cls, cursor_offset, current_line, **kwargs):
155-
cs = lineparts.current_string(cursor_offset, current_line)
176+
def matches(cls, cursor_offset, line, **kwargs):
177+
cs = lineparts.current_string(cursor_offset, line)
156178
if cs is None:
157179
return None
158180
start, end, text = cs
@@ -178,7 +200,7 @@ def format(cls, filename):
178200

179201
class AttrCompletion(BaseCompletionType):
180202
@classmethod
181-
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
203+
def matches(cls, cursor_offset, line, locals_, **kwargs):
182204
r = cls.locate(cursor_offset, line)
183205
if r is None:
184206
return None
@@ -195,7 +217,7 @@ def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
195217
break
196218
methodtext = text[-i:]
197219
matches = [''.join([text[:-i], m]) for m in
198-
attr_matches(methodtext, locals_, mode)]
220+
attr_matches(methodtext, locals_)]
199221

200222
#TODO add open paren for methods via _callable_prefix (or decide not to)
201223
# unless the first character is a _ filter out all attributes starting with a _
@@ -242,7 +264,7 @@ def matches(cls, cursor_offset, line, current_block, **kwargs):
242264

243265
class GlobalCompletion(BaseCompletionType):
244266
@classmethod
245-
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
267+
def matches(cls, cursor_offset, line, locals_, **kwargs):
246268
"""Compute matches when text is a simple name.
247269
Return a list of all keywords, built-in functions and names currently
248270
defined in self.namespace that match.
@@ -256,11 +278,11 @@ def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
256278
n = len(text)
257279
import keyword
258280
for word in keyword.kwlist:
259-
if method_match(word, n, text, mode):
281+
if method_match(word, n, text):
260282
hash[word] = 1
261283
for nspace in [__builtin__.__dict__, locals_]:
262284
for word, val in nspace.items():
263-
if method_match(word, len(text), text, mode) and word != "__builtins__":
285+
if method_match(word, len(text), text) and word != "__builtins__":
264286
hash[_callable_postfix(val, word)] = 1
265287
matches = hash.keys()
266288
matches.sort()
@@ -315,7 +337,7 @@ def safe_eval(expr, namespace):
315337
raise EvaluationError
316338

317339

318-
def attr_matches(text, namespace, autocomplete_mode):
340+
def attr_matches(text, namespace):
319341
"""Taken from rlcompleter.py and bent to my will.
320342
"""
321343

@@ -336,10 +358,10 @@ def attr_matches(text, namespace, autocomplete_mode):
336358
except EvaluationError:
337359
return []
338360
with inspection.AttrCleaner(obj):
339-
matches = attr_lookup(obj, expr, attr, autocomplete_mode)
361+
matches = attr_lookup(obj, expr, attr)
340362
return matches
341363

342-
def attr_lookup(obj, expr, attr, autocomplete_mode):
364+
def attr_lookup(obj, expr, attr):
343365
"""Second half of original attr_matches method factored out so it can
344366
be wrapped in a safe try/finally block in case anything bad happens to
345367
restore the original __getattribute__ method."""
@@ -356,7 +378,7 @@ def attr_lookup(obj, expr, attr, autocomplete_mode):
356378
matches = []
357379
n = len(attr)
358380
for word in words:
359-
if method_match(word, n, attr, autocomplete_mode) and word != "__builtins__":
381+
if method_match(word, n, attr) and word != "__builtins__":
360382
matches.append("%s.%s" % (expr, word))
361383
return matches
362384

@@ -367,14 +389,5 @@ def _callable_postfix(value, word):
367389
word += '('
368390
return word
369391

370-
#TODO use method_match everywhere instead of startswith to implement other completion modes
371-
# will also need to rewrite checking mode so cseq replace doesn't happen in frontends
372-
def method_match(word, size, text, autocomplete_mode):
373-
if autocomplete_mode == SIMPLE:
374-
return word[:size] == text
375-
elif autocomplete_mode == SUBSTRING:
376-
s = r'.*%s.*' % text
377-
return re.search(s, word)
378-
else:
379-
s = r'.*%s.*' % '.*'.join(list(text))
380-
return re.search(s, word)
392+
def method_match(word, size, text):
393+
return word[:size] == text

bpython/repl.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,15 @@ def set_docstring(self):
628628
if not self.docstring:
629629
self.docstring = None
630630

631+
# What complete() does:
632+
# Should we show the completion box? (are there matches, or is there a docstring to show?)
633+
# Some completions should always be shown, other only if tab=True
634+
# set the current docstring to the "current function's" docstring
635+
# Populate the matches_iter object with new matches from the current state
636+
# if none, clear the matches iterator
637+
# If exactly one match that is equal to current line, clear matches
638+
# If example one match and tab=True, then choose that and clear matches
639+
631640
def complete(self, tab=False):
632641
"""Construct a full list of possible completions and
633642
display them in a window. Also check if there's an available argspec
@@ -643,35 +652,30 @@ def complete(self, tab=False):
643652

644653
self.set_docstring()
645654

646-
matches, completer = autocomplete.get_completer(
647-
self.cursor_offset,
648-
self.current_line,
649-
self.interp.locals,
650-
self.argspec,
651-
'\n'.join(self.buffer + [self.current_line]),
652-
self.config.autocomplete_mode if hasattr(self.config, 'autocomplete_mode') else autocomplete.SIMPLE,
653-
self.config.complete_magic_methods)
655+
matches, completer = autocomplete.get_completer_bpython(
656+
cursor_offset=self.cursor_offset,
657+
line=self.current_line,
658+
locals_=self.interp.locals,
659+
argspec=self.argspec,
660+
current_block='\n'.join(self.buffer + [self.current_line]),
661+
complete_magic_methods=self.config.complete_magic_methods)
654662
#TODO implement completer.shown_before_tab == False (filenames shouldn't fill screen)
655663

656-
if (matches is None # no completion is relevant
657-
or len(matches) == 0): # a target for completion was found
658-
# but no matches were found
664+
if len(matches) == 0:
659665
self.matches_iter.clear()
660666
return bool(self.argspec)
661667

662668
self.matches_iter.update(self.cursor_offset,
663669
self.current_line, matches, completer)
664670

665671
if len(matches) == 1:
666-
self.matches_iter.next()
667-
if tab: # if this complete is being run for a tab key press, tab() to do the swap
668-
669-
self.cursor_offset, self.current_line = self.matches_iter.substitute_cseq()
670-
return Repl.complete(self)
671-
elif self.matches_iter.current_word == matches[0]:
672-
self.matches_iter.clear()
673-
return False
674-
return completer.shown_before_tab
672+
if tab: # if this complete is being run for a tab key press, substitute common sequence
673+
self._cursor_offset, self._current_line = self.matches_iter.substitute_cseq()
674+
return Repl.complete(self)
675+
elif self.matches_iter.current_word == matches[0]:
676+
self.matches_iter.clear()
677+
return False
678+
return completer.shown_before_tab
675679

676680
else:
677681
assert len(matches) > 1

0 commit comments

Comments
 (0)