X Tutup
Skip to content

Commit 3ee8696

Browse files
completion more uniform
1 parent bf87634 commit 3ee8696

File tree

5 files changed

+149
-86
lines changed

5 files changed

+149
-86
lines changed

bpython/autocomplete.py

Lines changed: 103 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,36 @@
4848
SUBSTRING = 'substring'
4949
FUZZY = 'fuzzy'
5050

51-
def attr_complete(text, namespace=None, config=None):
51+
MAGIC_METHODS = ["__%s__" % s for s in [
52+
"init", "repr", "str", "lt", "le", "eq", "ne", "gt", "ge", "cmp", "hash",
53+
"nonzero", "unicode", "getattr", "setattr", "get", "set","call", "len",
54+
"getitem", "setitem", "iter", "reversed", "contains", "add", "sub", "mul",
55+
"floordiv", "mod", "divmod", "pow", "lshift", "rshift", "and", "xor", "or",
56+
"div", "truediv", "neg", "pos", "abs", "invert", "complex", "int", "float",
57+
"oct", "hex", "index", "coerce", "enter", "exit"]]
58+
59+
60+
def attr_complete(text, namespace=None, mode=SIMPLE):
5261
"""Return list of matches """
5362
if namespace is None:
54-
namespace = __main__.__dict__ #TODO figure out if this __main__ still makes sense
63+
namespace = __main__.__dict__
64+
65+
assert '.' in text
66+
67+
for i in range(1, len(text) + 1):
68+
if text[-i] == '[':
69+
i -= 1
70+
break
71+
methodtext = text[-i:]
72+
matches = [''.join([text[:-i], m]) for m in
73+
attr_matches(methodtext, namespace, mode)]
74+
75+
# unless the first character is a _ filter out all attributes starting with a _
76+
if not text.split('.')[-1].startswith('_'):
77+
matches = [match for match in matches
78+
if not match.split('.')[-1].startswith('_')]
79+
return matches
5580

56-
if hasattr(config, 'autocomplete_mode'):
57-
autocomplete_mode = config.autocomplete_mode
58-
else:
59-
autocomplete_mode = SUBSTRING
60-
61-
if "." in text:
62-
# Examples: 'foo.b' or 'foo[bar.'
63-
for i in range(1, len(text) + 1):
64-
if text[-i] == '[':
65-
i -= 1
66-
break
67-
methodtext = text[-i:]
68-
return [''.join([text[:-i], m]) for m in
69-
attr_matches(methodtext, namespace, autocomplete_mode)]
70-
else:
71-
return global_matches(text, namespace, autocomplete_mode)
7281

7382
class SafeEvalFailed(Exception):
7483
"""If this object is returned, safe_eval failed"""
@@ -155,6 +164,8 @@ def global_matches(text, namespace, autocomplete_mode):
155164
matches.sort()
156165
return matches
157166

167+
#TODO use method_match everywhere instead of startswith to implement other completion modes
168+
# will also need to rewrite checking mode so cseq replace doesn't happen in frontends
158169
def method_match(word, size, text, autocomplete_mode):
159170
if autocomplete_mode == SIMPLE:
160171
return word[:size] == text
@@ -187,57 +198,45 @@ def last_part_of_filename(filename):
187198
def after_last_dot(name):
188199
return name.rstrip('.').rsplit('.')[-1]
189200

190-
def dict_key_format(filename):
191-
# dictionary key suggestions
192-
#items = [x.rstrip(']') for x in items]
193-
#if current_item:
194-
# current_item = current_item.rstrip(']')
195-
pass
196-
197-
def get_completer(cursor_offset, current_line, locals_, argspec, config, magic_methods):
201+
def get_completer(cursor_offset, current_line, locals_, argspec, full_code, mode, complete_magic_methods):
198202
"""Returns a list of matches and a class for what kind of completion is happening
199203
200204
If no completion type is relevant, returns None, None"""
201205

202-
#TODO use the smarter current_string() in Repl that knows about the buffer
203-
#TODO don't pass in config, pass in the settings themselves
206+
kwargs = {'locals_':locals_, 'argspec':argspec, 'full_code':full_code,
207+
'mode':mode, 'complete_magic_methods':complete_magic_methods}
204208

205-
matches = ImportCompletion.matches(cursor_offset, current_line)
206-
if matches is not None:
207-
return sorted(set(matches)), ImportCompletion
209+
# mutually exclusive matchers: if one returns [], don't go on
210+
for completer in [ImportCompletion, FilenameCompletion,
211+
MagicMethodCompletion, GlobalCompletion]:
212+
matches = completer.matches(cursor_offset, current_line, **kwargs)
213+
if matches is not None:
214+
return sorted(set(matches)), completer
208215

209-
matches = FilenameCompletion.matches(cursor_offset, current_line)
210-
if matches is not None:
211-
return sorted(set(matches)), FilenameCompletion
216+
# mutually exclusive if matches: If one of these returns [], try the next one
217+
for completer in [DictKeyCompletion]:
218+
matches = completer.matches(cursor_offset, current_line, **kwargs)
219+
if matches:
220+
return sorted(set(matches)), completer
212221

213-
matches = DictKeyCompletion.matches(cursor_offset, current_line, locals_=locals_, config=config)
214-
if matches:
215-
return sorted(set(matches)), DictKeyCompletion
222+
matches = AttrCompletion.matches(cursor_offset, current_line, **kwargs)
216223

217-
matches = AttrCompletion.matches(cursor_offset, current_line, locals_=locals_, config=config)
218-
if matches is not None:
219-
cw = AttrCompletion.locate(cursor_offset, current_line)[2]
220-
matches.extend(magic_methods(cw))
221-
if argspec:
222-
matches.extend(name + '=' for name in argspec[1][0]
223-
if isinstance(name, basestring) and name.startswith(cw))
224-
if py3:
225-
matches.extend(name + '=' for name in argspec[1][4]
226-
if name.startswith(cw))
224+
# cumulative completions - try them all
225+
# They all use current_word replacement and formatting
226+
current_word_matches = []
227+
for completer in [AttrCompletion, ParameterNameCompletion]:
228+
matches = completer.matches(cursor_offset, current_line, **kwargs)
229+
if matches is not None:
230+
current_word_matches.extend(matches)
227231

228-
# unless the first character is a _ filter out all attributes starting with a _
229-
if not cw.split('.')[-1].startswith('_'):
230-
matches = [match for match in matches
231-
if not match.split('.')[-1].startswith('_')]
232-
233-
return sorted(set(matches)), AttrCompletion
234-
235-
return None, None
232+
if len(current_word_matches) == 0:
233+
return None, None
234+
return sorted(set(current_word_matches)), AttrCompletion
236235

237236

238237
class BaseCompletionType(object):
239238
"""Describes different completion types"""
240-
def matches(cls, cursor_offset, line):
239+
def matches(cls, cursor_offset, line, **kwargs):
241240
"""Returns a list of possible matches given a line and cursor, or None
242241
if this completion type isn't applicable.
243242
@@ -268,14 +267,16 @@ def substitute(cls, cursor_offset, line, match):
268267
return result
269268

270269
class ImportCompletion(BaseCompletionType):
271-
matches = staticmethod(importcompletion.complete)
270+
@classmethod
271+
def matches(cls, cursor_offset, current_line, **kwargs):
272+
return importcompletion.complete(cursor_offset, current_line)
272273
locate = staticmethod(lineparts.current_word)
273274
format = staticmethod(after_last_dot)
274275

275276
class FilenameCompletion(BaseCompletionType):
276277
shown_before_tab = False
277278
@classmethod
278-
def matches(cls, cursor_offset, current_line):
279+
def matches(cls, cursor_offset, current_line, **kwargs):
279280
cs = lineparts.current_string(cursor_offset, current_line)
280281
if cs is None:
281282
return None
@@ -285,19 +286,19 @@ def matches(cls, cursor_offset, current_line):
285286

286287
class AttrCompletion(BaseCompletionType):
287288
@classmethod
288-
def matches(cls, cursor_offset, line, locals_, config):
289+
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
289290
r = cls.locate(cursor_offset, line)
290291
if r is None:
291292
return None
292293
cw = r[2]
293-
return attr_complete(cw, namespace=locals_, config=config)
294-
locate = staticmethod(lineparts.current_word)
294+
return attr_complete(cw, namespace=locals_, mode=mode)
295+
locate = staticmethod(lineparts.current_dotted_attribute)
295296
format = staticmethod(after_last_dot)
296297

297298
class DictKeyCompletion(BaseCompletionType):
298299
locate = staticmethod(lineparts.current_dict_key)
299300
@classmethod
300-
def matches(cls, cursor_offset, line, locals_, config):
301+
def matches(cls, cursor_offset, line, locals_, **kwargs):
301302
r = cls.locate(cursor_offset, line)
302303
if r is None:
303304
return None
@@ -313,3 +314,43 @@ def matches(cls, cursor_offset, line, locals_, config):
313314
@classmethod
314315
def format(cls, match):
315316
return match[:-1]
317+
318+
class MagicMethodCompletion(BaseCompletionType):
319+
locate = staticmethod(lineparts.current_method_definition_name)
320+
@classmethod
321+
def matches(cls, cursor_offset, line, full_code, **kwargs):
322+
r = cls.locate(cursor_offset, line)
323+
if r is None:
324+
return None
325+
if 'class' not in full_code:
326+
return None
327+
start, end, word = r
328+
return [name for name in MAGIC_METHODS if name.startswith(word)]
329+
330+
class GlobalCompletion(BaseCompletionType):
331+
@classmethod
332+
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
333+
r = cls.locate(cursor_offset, line)
334+
if r is None:
335+
return None
336+
start, end, word = r
337+
return global_matches(word, locals_, mode)
338+
locate = staticmethod(lineparts.current_single_word)
339+
340+
class ParameterNameCompletion(BaseCompletionType):
341+
@classmethod
342+
def matches(cls, cursor_offset, line, argspec, **kwargs):
343+
if not argspec:
344+
return None
345+
r = cls.locate(cursor_offset, line)
346+
if r is None:
347+
return None
348+
start, end, word = r
349+
if argspec:
350+
matches = [name + '=' for name in argspec[1][0]
351+
if isinstance(name, basestring) and name.startswith(word)]
352+
if py3:
353+
matches.extend(name + '=' for name in argspec[1][4]
354+
if name.startswith(word))
355+
return matches
356+
locate = staticmethod(lineparts.current_word)

bpython/config.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,6 @@
66
from bpython.keys import cli_key_dispatch as key_dispatch
77
from bpython.autocomplete import SIMPLE as default_completion
88

9-
MAGIC_METHODS = ", ".join("__%s__" % s for s in [
10-
"init", "repr", "str", "lt", "le", "eq", "ne", "gt", "ge", "cmp", "hash",
11-
"nonzero", "unicode", "getattr", "setattr", "get", "set","call", "len",
12-
"getitem", "setitem", "iter", "reversed", "contains", "add", "sub", "mul",
13-
"floordiv", "mod", "divmod", "pow", "lshift", "rshift", "and", "xor", "or",
14-
"div", "truediv", "neg", "pos", "abs", "invert", "complex", "int", "float",
15-
"oct", "hex", "index", "coerce", "enter", "exit"]
16-
)
17-
189
class Struct(object):
1910
"""Simple class for instantiating objects we can add arbitrary attributes
2011
to and use for various arbitrary things."""
@@ -55,7 +46,6 @@ def loadini(struct, configfile):
5546
'auto_display_list': True,
5647
'color_scheme': 'default',
5748
'complete_magic_methods' : True,
58-
'magic_methods' : MAGIC_METHODS,
5949
'autocomplete_mode': default_completion,
6050
'dedent_after': 1,
6151
'flush_output': True,
@@ -155,8 +145,6 @@ def loadini(struct, configfile):
155145

156146
struct.complete_magic_methods = config.getboolean('general',
157147
'complete_magic_methods')
158-
methods = config.get('general', 'magic_methods')
159-
struct.magic_methods = [meth.strip() for meth in methods.split(",")]
160148
struct.autocomplete_mode = config.get('general', 'autocomplete_mode')
161149
struct.save_append_py = config.getboolean('general', 'save_append_py')
162150

bpython/line.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,28 @@ def current_import(cursor_offset, line):
125125
end = baseline.end() + m.end(1)
126126
if start < cursor_offset and end >= cursor_offset:
127127
return start, end, m.group(1)
128+
129+
def current_method_definition_name(cursor_offset, line):
130+
"""The name of a method being defined"""
131+
matches = re.finditer("def\s+([a-zA-Z_][\w]*)", line)
132+
for m in matches:
133+
if (m.start(1) <= cursor_offset and m.end(1) >= cursor_offset):
134+
return m.start(1), m.end(1), m.group(1)
135+
return None
136+
137+
def current_single_word(cursor_offset, line):
138+
"""the un-dotted word just before or under the cursor"""
139+
matches = re.finditer(r"(?<![.])\b([a-zA-Z_][\w]*)", line)
140+
for m in matches:
141+
if (m.start(1) <= cursor_offset and m.end(1) >= cursor_offset):
142+
return m.start(1), m.end(1), m.group(1)
143+
return None
144+
145+
def current_dotted_attribute(cursor_offset, line):
146+
"""The dotted attribute-object pair before the cursor"""
147+
match = current_word(cursor_offset, line)
148+
if match is None: return None
149+
start, end, word = match
150+
if '.' in word[1:]:
151+
return start, end, word
152+
return None

bpython/repl.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -601,16 +601,6 @@ def set_docstring(self):
601601
if not self.docstring:
602602
self.docstring = None
603603

604-
def magic_method_completions(self, cw):
605-
#TODO move this to autocompletion and pass in the buffer
606-
if (self.config.complete_magic_methods and self.buffer and
607-
self.buffer[0].startswith("class ") and
608-
self.current_line.lstrip().startswith("def ")):
609-
return [name for name in self.config.magic_methods
610-
if name.startswith(cw)]
611-
else:
612-
return []
613-
614604
def complete(self, tab=False):
615605
"""Construct a full list of possible completions and construct and
616606
display them in a window. Also check if there's an available argspec
@@ -631,8 +621,10 @@ def complete(self, tab=False):
631621
self.current_line,
632622
self.interp.locals,
633623
self.argspec,
634-
self.config,
635-
self.magic_method_completions)
624+
'\n'.join(self.buffer + [self.current_line]),
625+
self.config.autocomplete_mode if hasattr(self.config, 'autocomplete_mode') else autocomplete.SIMPLE,
626+
self.config.complete_magic_methods)
627+
#TODO implement completer.shown_before_tab == False (filenames shouldn't fill screen)
636628

637629
if (matches is None # no completion is relevant
638630
or len(matches) == 0): # a target for completion was found

bpython/test/test_line_properties.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33

44
from bpython.line import current_word, current_dict_key, current_dict, current_string, current_object, current_object_attribute, current_from_import_from, current_from_import_import, current_import
5+
from bpython.line import current_word, current_dict_key, current_dict, current_string, current_object, current_object_attribute, current_from_import_from, current_from_import_import, current_import, current_method_definition_name, current_single_word
56

67

78
def cursor(s):
@@ -236,5 +237,21 @@ def test_simple(self):
236237
self.assertAccess('if True: import <xml.do|m.minidom>')
237238
self.assertAccess('if True: import <xml.do|m.minidom> as something')
238239

240+
class TestMethodDefinitionName(LineTestCase):
241+
def setUp(self):
242+
self.func = current_method_definition_name
243+
def test_simple(self):
244+
self.assertAccess('def <foo|>')
245+
self.assertAccess(' def bar(x, y)|:')
246+
self.assertAccess(' def <bar|>(x, y)')
247+
248+
class TestMethodDefinitionName(LineTestCase):
249+
def setUp(self):
250+
self.func = current_single_word
251+
def test_simple(self):
252+
self.assertAccess('foo.bar|')
253+
self.assertAccess('.foo|')
254+
self.assertAccess(' <foo|>')
255+
239256
if __name__ == '__main__':
240257
unittest.main()

0 commit comments

Comments
 (0)
X Tutup