Skip to content

Commit 78f5304

Browse files
Merge branch 'clean-up-completion'
Conflicts: bpython/autocomplete.py bpython/repl.py
2 parents 6d2574c + aea1a25 commit 78f5304

File tree

4 files changed

+231
-95
lines changed

4 files changed

+231
-95
lines changed

bpython/autocomplete.py

Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import re
3131
import os
3232
from glob import glob
33+
from functools import partial
3334
from bpython import inspection
3435
from bpython import importcompletion
3536
from bpython._py3compat import py3
@@ -43,7 +44,7 @@
4344

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

56-
def get_completer(cursor_offset, current_line, locals_, argspec, full_code, mode, complete_magic_methods):
57-
"""Returns a list of matches and a class for what kind of completion is happening
58-
59-
If no completion type is relevant, returns None, None
60-
61-
argspec is an output of inspect.getargspec
57+
def get_completer(completers, cursor_offset, line, **kwargs):
58+
"""Returns a list of matches and an applicable completer
59+
60+
If no matches available, returns a tuple of an empty list and None
61+
62+
kwargs (all required):
63+
cursor_offset is the current cursor column
64+
line is a string of the current line
65+
locals_ is a dictionary of the environment
66+
argspec is an inspect.ArgSpec instance for the current function where
67+
the cursor is
68+
current_block is the possibly multiline not-yet-evaluated block of
69+
code which the current line is part of
70+
mode is one of SIMPLE, SUBSTRING or FUZZY - ways to find matches
71+
complete_magic_methods is a bool of whether we ought to complete
72+
double underscore methods like __len__ in method signatures
6273
"""
6374

64-
kwargs = {'locals_':locals_, 'argspec':argspec, 'full_code':full_code,
65-
'mode':mode, 'complete_magic_methods':complete_magic_methods}
66-
67-
# mutually exclusive if matches: If one of these returns [], try the next one
68-
for completer in [DictKeyCompletion]:
69-
matches = completer.matches(cursor_offset, current_line, **kwargs)
70-
if matches:
71-
return sorted(set(matches)), completer
72-
73-
# mutually exclusive matchers: if one returns [], don't go on
74-
for completer in [StringLiteralAttrCompletion, ImportCompletion,
75-
FilenameCompletion, MagicMethodCompletion, GlobalCompletion]:
76-
matches = completer.matches(cursor_offset, current_line, **kwargs)
75+
for completer in completers:
76+
matches = completer.matches(cursor_offset, line, **kwargs)
7777
if matches is not None:
78-
return sorted(set(matches)), completer
79-
80-
matches = AttrCompletion.matches(cursor_offset, current_line, **kwargs)
81-
82-
# cumulative completions - try them all
83-
# They all use current_word replacement and formatting
84-
current_word_matches = []
85-
for completer in [AttrCompletion, ParameterNameCompletion]:
86-
matches = completer.matches(cursor_offset, current_line, **kwargs)
87-
if matches is not None:
88-
current_word_matches.extend(matches)
89-
90-
if len(current_word_matches) == 0:
91-
return None, None
92-
return sorted(set(current_word_matches)), AttrCompletion
78+
return matches, (completer if matches else None)
79+
return [], None
80+
81+
def get_completer_bpython(**kwargs):
82+
""""""
83+
return get_completer([DictKeyCompletion,
84+
StringLiteralAttrCompletion,
85+
ImportCompletion,
86+
FilenameCompletion,
87+
MagicMethodCompletion,
88+
GlobalCompletion,
89+
CumulativeCompleter([AttrCompletion, ParameterNameCompletion])],
90+
**kwargs)
9391

9492
class BaseCompletionType(object):
9593
"""Describes different completion types"""
94+
@classmethod
9695
def matches(cls, cursor_offset, line, **kwargs):
9796
"""Returns a list of possible matches given a line and cursor, or None
9897
if this completion type isn't applicable.
9998
10099
ie, import completion doesn't make sense if there cursor isn't after
101-
an import or from statement
100+
an import or from statement, so it ought to return None.
102101
103102
Completion types are used to:
104-
* `locate(cur, line)` their target word to replace given a line and cursor
103+
* `locate(cur, line)` their initial target word to replace given a line and cursor
105104
* find `matches(cur, line)` that might replace that word
106105
* `format(match)` matches to be displayed to the user
107106
* determine whether suggestions should be `shown_before_tab`
108107
* `substitute(cur, line, match)` in a match for what's found with `target`
109108
"""
110109
raise NotImplementedError
110+
@classmethod
111111
def locate(cls, cursor_offset, line):
112112
"""Returns a start, stop, and word given a line and cursor, or None
113113
if no target for this type of completion is found under the cursor"""
@@ -116,25 +116,58 @@ def locate(cls, cursor_offset, line):
116116
def format(cls, word):
117117
return word
118118
shown_before_tab = True # whether suggestions should be shown before the
119-
# user hits tab, or only once that has happened
119+
# user hits tab, or only once that has happened
120120
def substitute(cls, cursor_offset, line, match):
121121
"""Returns a cursor offset and line with match swapped in"""
122122
start, end, word = cls.locate(cursor_offset, line)
123123
result = start + len(match), line[:start] + match + line[end:]
124124
return result
125125

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

133166
class FilenameCompletion(BaseCompletionType):
134167
shown_before_tab = False
135168
@classmethod
136-
def matches(cls, cursor_offset, current_line, **kwargs):
137-
cs = lineparts.current_string(cursor_offset, current_line)
169+
def matches(cls, cursor_offset, line, **kwargs):
170+
cs = lineparts.current_string(cursor_offset, line)
138171
if cs is None:
139172
return None
140173
start, end, text = cs
@@ -160,7 +193,7 @@ def format(cls, filename):
160193

161194
class AttrCompletion(BaseCompletionType):
162195
@classmethod
163-
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
196+
def matches(cls, cursor_offset, line, locals_, **kwargs):
164197
r = cls.locate(cursor_offset, line)
165198
if r is None:
166199
return None
@@ -177,7 +210,7 @@ def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
177210
break
178211
methodtext = text[-i:]
179212
matches = [''.join([text[:-i], m]) for m in
180-
attr_matches(methodtext, locals_, mode)]
213+
attr_matches(methodtext, locals_)]
181214

182215
#TODO add open paren for methods via _callable_prefix (or decide not to)
183216
# unless the first character is a _ filter out all attributes starting with a _
@@ -213,18 +246,18 @@ def format(cls, match):
213246
class MagicMethodCompletion(BaseCompletionType):
214247
locate = staticmethod(lineparts.current_method_definition_name)
215248
@classmethod
216-
def matches(cls, cursor_offset, line, full_code, **kwargs):
249+
def matches(cls, cursor_offset, line, current_block, **kwargs):
217250
r = cls.locate(cursor_offset, line)
218251
if r is None:
219252
return None
220-
if 'class' not in full_code:
253+
if 'class' not in current_block:
221254
return None
222255
start, end, word = r
223256
return [name for name in MAGIC_METHODS if name.startswith(word)]
224257

225258
class GlobalCompletion(BaseCompletionType):
226259
@classmethod
227-
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
260+
def matches(cls, cursor_offset, line, locals_, **kwargs):
228261
"""Compute matches when text is a simple name.
229262
Return a list of all keywords, built-in functions and names currently
230263
defined in self.namespace that match.
@@ -238,11 +271,11 @@ def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
238271
n = len(text)
239272
import keyword
240273
for word in keyword.kwlist:
241-
if method_match(word, n, text, mode):
274+
if method_match(word, n, text):
242275
hash[word] = 1
243276
for nspace in [__builtin__.__dict__, locals_]:
244277
for word, val in nspace.items():
245-
if method_match(word, len(text), text, mode) and word != "__builtins__":
278+
if method_match(word, len(text), text) and word != "__builtins__":
246279
hash[_callable_postfix(val, word)] = 1
247280
matches = hash.keys()
248281
matches.sort()
@@ -299,7 +332,7 @@ def safe_eval(expr, namespace):
299332

300333
attr_matches_re = re.compile(r"(\w+(\.\w+)*)\.(\w*)")
301334

302-
def attr_matches(text, namespace, autocomplete_mode):
335+
def attr_matches(text, namespace):
303336
"""Taken from rlcompleter.py and bent to my will.
304337
"""
305338

@@ -320,10 +353,10 @@ def attr_matches(text, namespace, autocomplete_mode):
320353
except EvaluationError:
321354
return []
322355
with inspection.AttrCleaner(obj):
323-
matches = attr_lookup(obj, expr, attr, autocomplete_mode)
356+
matches = attr_lookup(obj, expr, attr)
324357
return matches
325358

326-
def attr_lookup(obj, expr, attr, autocomplete_mode):
359+
def attr_lookup(obj, expr, attr):
327360
"""Second half of original attr_matches method factored out so it can
328361
be wrapped in a safe try/finally block in case anything bad happens to
329362
restore the original __getattribute__ method."""
@@ -340,7 +373,7 @@ def attr_lookup(obj, expr, attr, autocomplete_mode):
340373
matches = []
341374
n = len(attr)
342375
for word in words:
343-
if method_match(word, n, attr, autocomplete_mode) and word != "__builtins__":
376+
if method_match(word, n, attr) and word != "__builtins__":
344377
matches.append("%s.%s" % (expr, word))
345378
return matches
346379

@@ -351,14 +384,5 @@ def _callable_postfix(value, word):
351384
word += '('
352385
return word
353386

354-
#TODO use method_match everywhere instead of startswith to implement other completion modes
355-
# will also need to rewrite checking mode so cseq replace doesn't happen in frontends
356-
def method_match(word, size, text, autocomplete_mode):
357-
if autocomplete_mode == SIMPLE:
358-
return word[:size] == text
359-
elif autocomplete_mode == SUBSTRING:
360-
s = r'.*%s.*' % text
361-
return re.search(s, word)
362-
else:
363-
s = r'.*%s.*' % '.*'.join(list(text))
364-
return re.search(s, word)
387+
def method_match(word, size, text):
388+
return word[:size] == text

bpython/curtsiesfrontend/repl.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -755,9 +755,12 @@ def add_normal_character(self, char):
755755
if self.incremental_search_mode:
756756
self.add_to_incremental_search(char)
757757
else:
758-
self.current_line = (self.current_line[:self.cursor_offset] +
759-
char +
760-
self.current_line[self.cursor_offset:])
758+
self._set_current_line((self.current_line[:self.cursor_offset] +
759+
char +
760+
self.current_line[self.cursor_offset:]),
761+
update_completion=False,
762+
reset_rl_history=False,
763+
clear_special_mode=False)
761764
self.cursor_offset += 1
762765
if self.config.cli_trim_prompts and self.current_line.startswith(self.ps1):
763766
self.current_line = self.current_line[4:]
@@ -1237,14 +1240,13 @@ def _get_cursor_offset(self):
12371240
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=False, clear_special_mode=True):
12381241
if self._cursor_offset == offset:
12391242
return
1240-
if update_completion:
1241-
self.update_completion()
12421243
if reset_rl_history:
12431244
self.rl_history.reset()
12441245
if clear_special_mode:
12451246
self.incremental_search_mode = None
12461247
self._cursor_offset = offset
1247-
self.update_completion()
1248+
if update_completion:
1249+
self.update_completion()
12481250
self.unhighlight_paren()
12491251

12501252
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,

bpython/repl.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,17 @@ def set_docstring(self):
498498
if not self.docstring:
499499
self.docstring = None
500500

501+
# What complete() does:
502+
# Should we show the completion box? (are there matches, or is there a docstring to show?)
503+
# Some completions should always be shown, other only if tab=True
504+
# set the current docstring to the "current function's" docstring
505+
# Populate the matches_iter object with new matches from the current state
506+
# if none, clear the matches iterator
507+
# If exactly one match that is equal to current line, clear matches
508+
# If example one match and tab=True, then choose that and clear matches
509+
501510
def complete(self, tab=False):
502-
"""Construct a full list of possible completions and construct and
511+
"""Construct a full list of possible completions and
503512
display them in a window. Also check if there's an available argspec
504513
(via the inspect module) and bang that on top of the completions too.
505514
The return value is whether the list_win is visible or not.
@@ -513,35 +522,30 @@ def complete(self, tab=False):
513522

514523
self.set_docstring()
515524

516-
matches, completer = autocomplete.get_completer(
517-
self.cursor_offset,
518-
self.current_line,
519-
self.interp.locals,
520-
self.argspec,
521-
'\n'.join(self.buffer + [self.current_line]),
522-
self.config.autocomplete_mode,
523-
self.config.complete_magic_methods)
525+
matches, completer = autocomplete.get_completer_bpython(
526+
cursor_offset=self.cursor_offset,
527+
line=self.current_line,
528+
locals_=self.interp.locals,
529+
argspec=self.argspec,
530+
current_block='\n'.join(self.buffer + [self.current_line]),
531+
complete_magic_methods=self.config.complete_magic_methods)
524532
#TODO implement completer.shown_before_tab == False (filenames shouldn't fill screen)
525533

526-
if (matches is None # no completion is relevant
527-
or len(matches) == 0): # a target for completion was found
528-
# but no matches were found
534+
if len(matches) == 0:
529535
self.matches_iter.clear()
530536
return bool(self.argspec)
531537

532538
self.matches_iter.update(self.cursor_offset,
533539
self.current_line, matches, completer)
534540

535541
if len(matches) == 1:
536-
self.matches_iter.next()
537-
if tab: # if this complete is being run for a tab key press, tab() to do the swap
538-
539-
self.cursor_offset, self.current_line = self.matches_iter.substitute_cseq()
540-
return Repl.complete(self)
541-
elif self.matches_iter.current_word == matches[0]:
542-
self.matches_iter.clear()
543-
return False
544-
return completer.shown_before_tab
542+
if tab: # if this complete is being run for a tab key press, substitute common sequence
543+
self._cursor_offset, self._current_line = self.matches_iter.substitute_cseq()
544+
return Repl.complete(self)
545+
elif self.matches_iter.current_word == matches[0]:
546+
self.matches_iter.clear()
547+
return False
548+
return completer.shown_before_tab
545549

546550
else:
547551
assert len(matches) > 1

0 commit comments

Comments
 (0)