3030import re
3131import os
3232from glob import glob
33+ from functools import partial
3334from bpython import inspection
3435from bpython import importcompletion
3536from bpython ._py3compat import py3
4344
4445MAGIC_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" ,
5354def 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
9492class 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+
126159class 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
133166class 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
161194class 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):
213246class 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
225258class 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
300333attr_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
0 commit comments