Skip to content

Commit adb00a0

Browse files
committed
Be consistent with completion classes
Currently there is a mix of classes that are used with staticmethods and classmethods, and others that need to be instantiated. This unifies the use of all these classes. Signed-off-by: Sebastian Ramacher <[email protected]>
1 parent f357e4c commit adb00a0

File tree

3 files changed

+141
-101
lines changed

3 files changed

+141
-101
lines changed

bpython/autocomplete.py

Lines changed: 121 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -53,44 +53,14 @@
5353
def after_last_dot(name):
5454
return name.rstrip('.').rsplit('.')[-1]
5555

56-
def get_completer(completers, cursor_offset, line, **kwargs):
57-
"""Returns a list of matches and an applicable completer
58-
59-
If no matches available, returns a tuple of an empty list and None
60-
61-
kwargs (all required):
62-
cursor_offset is the current cursor column
63-
line is a string of the current line
64-
locals_ is a dictionary of the environment
65-
argspec is an inspect.ArgSpec instance for the current function where
66-
the cursor is
67-
current_block is the possibly multiline not-yet-evaluated block of
68-
code which the current line is part of
69-
complete_magic_methods is a bool of whether we ought to complete
70-
double underscore methods like __len__ in method signatures
71-
"""
72-
73-
for completer in completers:
74-
matches = completer.matches(cursor_offset, line, **kwargs)
75-
if matches is not None:
76-
return matches, (completer if matches else None)
77-
return [], None
78-
79-
def get_completer_bpython(**kwargs):
80-
""""""
81-
return get_completer([DictKeyCompletion,
82-
StringLiteralAttrCompletion,
83-
ImportCompletion,
84-
FilenameCompletion,
85-
MagicMethodCompletion,
86-
GlobalCompletion,
87-
CumulativeCompleter([AttrCompletion, ParameterNameCompletion])],
88-
**kwargs)
8956

9057
class BaseCompletionType(object):
9158
"""Describes different completion types"""
92-
@classmethod
93-
def matches(cls, cursor_offset, line, **kwargs):
59+
60+
def __init__(self, shown_before_tab=True):
61+
self._shown_before_tab = shown_before_tab
62+
63+
def matches(self, cursor_offset, line, **kwargs):
9464
"""Returns a list of possible matches given a line and cursor, or None
9565
if this completion type isn't applicable.
9666
@@ -105,37 +75,42 @@ def matches(cls, cursor_offset, line, **kwargs):
10575
* `substitute(cur, line, match)` in a match for what's found with `target`
10676
"""
10777
raise NotImplementedError
108-
@classmethod
109-
def locate(cls, cursor_offset, line):
78+
79+
def locate(self, cursor_offset, line):
11080
"""Returns a start, stop, and word given a line and cursor, or None
11181
if no target for this type of completion is found under the cursor"""
11282
raise NotImplementedError
113-
@classmethod
114-
def format(cls, word):
83+
84+
def format(self, word):
11585
return word
116-
shown_before_tab = True # whether suggestions should be shown before the
117-
# user hits tab, or only once that has happened
118-
def substitute(cls, cursor_offset, line, match):
86+
87+
def substitute(self, cursor_offset, line, match):
11988
"""Returns a cursor offset and line with match swapped in"""
120-
start, end, word = cls.locate(cursor_offset, line)
89+
start, end, word = self.locate(cursor_offset, line)
12190
result = start + len(match), line[:start] + match + line[end:]
12291
return result
12392

124-
class CumulativeCompleter(object):
93+
@property
94+
def shown_before_tab(self):
95+
"""Whether suggestions should be shown before the user hits tab, or only
96+
once that has happened."""
97+
return self._shown_before_tab
98+
99+
class CumulativeCompleter(BaseCompletionType):
125100
"""Returns combined matches from several completers"""
101+
126102
def __init__(self, completers):
127103
if not completers:
128104
raise ValueError("CumulativeCompleter requires at least one completer")
129105
self._completers = completers
130-
self.shown_before_tab = True
131106

132-
@property
133-
def locate(self):
134-
return self._completers[0].locate if self._completers else lambda *args: None
107+
super(CumulativeCompleter, self).__init__(True)
135108

136-
@property
137-
def format(self):
138-
return self._completers[0].format if self._completers else lambda s: s
109+
def locate(self, current_offset, line):
110+
return self._completers[0].locate(current_offset, line)
111+
112+
def format(self, word):
113+
return self._completers[0].format(word)
139114

140115
def matches(self, cursor_offset, line, locals_, argspec, current_block, complete_magic_methods):
141116
all_matches = []
@@ -155,16 +130,22 @@ def matches(self, cursor_offset, line, locals_, argspec, current_block, complete
155130

156131

157132
class ImportCompletion(BaseCompletionType):
158-
@classmethod
159-
def matches(cls, cursor_offset, line, **kwargs):
133+
134+
def matches(self, cursor_offset, line, **kwargs):
160135
return importcompletion.complete(cursor_offset, line)
161-
locate = staticmethod(lineparts.current_word)
162-
format = staticmethod(after_last_dot)
136+
137+
def locate(self, current_offset, line):
138+
return lineparts.current_word(current_offset, line)
139+
140+
def format(self, word):
141+
return after_last_dot(word)
163142

164143
class FilenameCompletion(BaseCompletionType):
165-
shown_before_tab = False
166-
@classmethod
167-
def matches(cls, cursor_offset, line, **kwargs):
144+
145+
def __init__(self):
146+
super(FilenameCompletion, self).__init__(False)
147+
148+
def matches(self, cursor_offset, line, **kwargs):
168149
cs = lineparts.current_string(cursor_offset, line)
169150
if cs is None:
170151
return None
@@ -180,19 +161,20 @@ def matches(cls, cursor_offset, line, **kwargs):
180161
matches.append(filename)
181162
return matches
182163

183-
locate = staticmethod(lineparts.current_string)
184-
@classmethod
185-
def format(cls, filename):
164+
def locate(self, current_offset, line):
165+
return lineparts.current_string(current_offset, line)
166+
167+
def format(self, filename):
186168
filename.rstrip(os.sep).rsplit(os.sep)[-1]
187169
if os.sep in filename[:-1]:
188170
return filename[filename.rindex(os.sep, 0, -1)+1:]
189171
else:
190172
return filename
191173

192174
class AttrCompletion(BaseCompletionType):
193-
@classmethod
194-
def matches(cls, cursor_offset, line, locals_, **kwargs):
195-
r = cls.locate(cursor_offset, line)
175+
176+
def matches(self, cursor_offset, line, locals_, **kwargs):
177+
r = self.locate(cursor_offset, line)
196178
if r is None:
197179
return None
198180
text = r[2]
@@ -217,14 +199,16 @@ def matches(cls, cursor_offset, line, locals_, **kwargs):
217199
if not match.split('.')[-1].startswith('_')]
218200
return matches
219201

220-
locate = staticmethod(lineparts.current_dotted_attribute)
221-
format = staticmethod(after_last_dot)
202+
def locate(self, current_offset, line):
203+
return lineparts.current_dotted_attribute(current_offset, line)
204+
205+
def format(self, word):
206+
return after_last_dot(word)
222207

223208
class DictKeyCompletion(BaseCompletionType):
224-
locate = staticmethod(lineparts.current_dict_key)
225-
@classmethod
226-
def matches(cls, cursor_offset, line, locals_, **kwargs):
227-
r = cls.locate(cursor_offset, line)
209+
210+
def matches(self, cursor_offset, line, locals_, **kwargs):
211+
r = self.locate(cursor_offset, line)
228212
if r is None:
229213
return None
230214
start, end, orig = r
@@ -237,30 +221,35 @@ def matches(cls, cursor_offset, line, locals_, **kwargs):
237221
return ["{!r}]".format(k) for k in obj.keys() if repr(k).startswith(orig)]
238222
else:
239223
return []
240-
@classmethod
241-
def format(cls, match):
224+
225+
def locate(self, current_offset, line):
226+
return lineparts.current_dict_key(current_offset, line)
227+
228+
def format(self, match):
242229
return match[:-1]
243230

244231
class MagicMethodCompletion(BaseCompletionType):
245-
locate = staticmethod(lineparts.current_method_definition_name)
246-
@classmethod
247-
def matches(cls, cursor_offset, line, current_block, **kwargs):
248-
r = cls.locate(cursor_offset, line)
232+
233+
def matches(self, cursor_offset, line, current_block, **kwargs):
234+
r = self.locate(cursor_offset, line)
249235
if r is None:
250236
return None
251237
if 'class' not in current_block:
252238
return None
253239
start, end, word = r
254240
return [name for name in MAGIC_METHODS if name.startswith(word)]
255241

242+
def locate(self, current_offset, line):
243+
return lineparts.current_method_definition_name(current_offset, line)
244+
256245
class GlobalCompletion(BaseCompletionType):
257-
@classmethod
258-
def matches(cls, cursor_offset, line, locals_, **kwargs):
246+
247+
def matches(self, cursor_offset, line, locals_, **kwargs):
259248
"""Compute matches when text is a simple name.
260249
Return a list of all keywords, built-in functions and names currently
261250
defined in self.namespace that match.
262251
"""
263-
r = cls.locate(cursor_offset, line)
252+
r = self.locate(cursor_offset, line)
264253
if r is None:
265254
return None
266255
start, end, text = r
@@ -279,14 +268,15 @@ def matches(cls, cursor_offset, line, locals_, **kwargs):
279268
matches.sort()
280269
return matches
281270

282-
locate = staticmethod(lineparts.current_single_word)
271+
def locate(self, current_offset, line):
272+
return lineparts.current_single_word(current_offset, line)
283273

284274
class ParameterNameCompletion(BaseCompletionType):
285-
@classmethod
286-
def matches(cls, cursor_offset, line, argspec, **kwargs):
275+
276+
def matches(self, cursor_offset, line, argspec, **kwargs):
287277
if not argspec:
288278
return None
289-
r = cls.locate(cursor_offset, line)
279+
r = self.locate(cursor_offset, line)
290280
if r is None:
291281
return None
292282
start, end, word = r
@@ -297,13 +287,14 @@ def matches(cls, cursor_offset, line, argspec, **kwargs):
297287
matches.extend(name + '=' for name in argspec[1][4]
298288
if name.startswith(word))
299289
return matches
300-
locate = staticmethod(lineparts.current_word)
290+
291+
def locate(self, current_offset, line):
292+
return lineparts.current_word(current_offset, line)
301293

302294
class StringLiteralAttrCompletion(BaseCompletionType):
303-
locate = staticmethod(lineparts.current_string_literal_attr)
304-
@classmethod
305-
def matches(cls, cursor_offset, line, **kwargs):
306-
r = cls.locate(cursor_offset, line)
295+
296+
def matches(self, cursor_offset, line, **kwargs):
297+
r = self.locate(cursor_offset, line)
307298
if r is None:
308299
return None
309300
start, end, word = r
@@ -313,6 +304,48 @@ def matches(cls, cursor_offset, line, **kwargs):
313304
return [match for match in matches if not match.startswith('_')]
314305
return matches
315306

307+
def locate(self, current_offset, line):
308+
return lineparts.current_string_literal_attr(current_offset, line)
309+
310+
311+
def get_completer(completers, cursor_offset, line, **kwargs):
312+
"""Returns a list of matches and an applicable completer
313+
314+
If no matches available, returns a tuple of an empty list and None
315+
316+
kwargs (all required):
317+
cursor_offset is the current cursor column
318+
line is a string of the current line
319+
locals_ is a dictionary of the environment
320+
argspec is an inspect.ArgSpec instance for the current function where
321+
the cursor is
322+
current_block is the possibly multiline not-yet-evaluated block of
323+
code which the current line is part of
324+
complete_magic_methods is a bool of whether we ought to complete
325+
double underscore methods like __len__ in method signatures
326+
"""
327+
328+
for completer in completers:
329+
matches = completer.matches(cursor_offset, line, **kwargs)
330+
if matches is not None:
331+
return matches, (completer if matches else None)
332+
return [], None
333+
334+
BPYTHON_COMPLETER = (
335+
DictKeyCompletion(),
336+
StringLiteralAttrCompletion(),
337+
ImportCompletion(),
338+
FilenameCompletion(),
339+
MagicMethodCompletion(),
340+
GlobalCompletion(),
341+
CumulativeCompleter((AttrCompletion(), ParameterNameCompletion()))
342+
)
343+
344+
def get_completer_bpython(**kwargs):
345+
""""""
346+
return get_completer(BPYTHON_COMPLETER,
347+
**kwargs)
348+
316349

317350
class EvaluationError(Exception):
318351
"""Raised if an exception occurred in safe_eval."""

bpython/test/test_autocomplete.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def test_catches_syntax_error(self):
2020
class TestFormatters(unittest.TestCase):
2121

2222
def test_filename(self):
23-
last_part_of_filename = autocomplete.FilenameCompletion.format
23+
completer = autocomplete.FilenameCompletion()
24+
last_part_of_filename = completer.format
2425
self.assertEqual(last_part_of_filename('abc'), 'abc')
2526
self.assertEqual(last_part_of_filename('abc/'), 'abc/')
2627
self.assertEqual(last_part_of_filename('abc/efg'), 'efg')
@@ -98,42 +99,46 @@ def test_two_completers_get_both(self):
9899

99100
class TestFilenameCompletion(unittest.TestCase):
100101

102+
def setUp(self):
103+
self.completer = autocomplete.FilenameCompletion()
104+
101105
def test_locate_fails_when_not_in_string(self):
102-
self.assertEqual(autocomplete.FilenameCompletion.locate(4, "abcd"), None)
106+
self.assertEqual(self.completer.locate(4, "abcd"), None)
103107

104108
def test_locate_succeeds_when_in_string(self):
105-
self.assertEqual(autocomplete.FilenameCompletion.locate(4, "a'bc'd"), (2, 4, 'bc'))
109+
self.assertEqual(self.completer.locate(4, "a'bc'd"), (2, 4, 'bc'))
106110

107111
@mock.patch('bpython.autocomplete.glob', new=lambda text: [])
108112
def test_match_returns_none_if_not_in_string(self):
109-
self.assertEqual(autocomplete.FilenameCompletion.matches(2, 'abcd'), None)
113+
self.assertEqual(self.completer.matches(2, 'abcd'), None)
110114

111115
@mock.patch('bpython.autocomplete.glob', new=lambda text: [])
112116
def test_match_returns_empty_list_when_no_files(self):
113-
self.assertEqual(autocomplete.FilenameCompletion.matches(2, '"a'), [])
117+
self.assertEqual(self.completer.matches(2, '"a'), [])
114118

115119
@mock.patch('bpython.autocomplete.glob', new=lambda text: ['abcde', 'aaaaa'])
116120
@mock.patch('os.path.expanduser', new=lambda text: text)
117121
@mock.patch('os.path.isdir', new=lambda text: False)
118122
@mock.patch('os.path.sep', new='/')
119123
def test_match_returns_files_when_files_exist(self):
120-
self.assertEqual(autocomplete.FilenameCompletion.matches(2, '"x'), ['abcde', 'aaaaa'])
124+
self.assertEqual(self.completer.matches(2, '"x'), ['abcde', 'aaaaa'])
121125

122126
@mock.patch('bpython.autocomplete.glob', new=lambda text: ['abcde', 'aaaaa'])
123127
@mock.patch('os.path.expanduser', new=lambda text: text)
124128
@mock.patch('os.path.isdir', new=lambda text: True)
125129
@mock.patch('os.path.sep', new='/')
126130
def test_match_returns_dirs_when_dirs_exist(self):
127-
self.assertEqual(autocomplete.FilenameCompletion.matches(2, '"x'), ['abcde/', 'aaaaa/'])
131+
self.assertEqual(self.completer.matches(2, '"x'), ['abcde/', 'aaaaa/'])
128132

129133
@mock.patch('bpython.autocomplete.glob', new=lambda text: ['/expand/ed/abcde', '/expand/ed/aaaaa'])
130134
@mock.patch('os.path.expanduser', new=lambda text: text.replace('~', '/expand/ed'))
131135
@mock.patch('os.path.isdir', new=lambda text: False)
132136
@mock.patch('os.path.sep', new='/')
133137
def test_tilde_stays_pretty(self):
134-
self.assertEqual(autocomplete.FilenameCompletion.matches(4, '"~/a'), ['~/abcde', '~/aaaaa'])
138+
self.assertEqual(self.completer.matches(4, '"~/a'), ['~/abcde', '~/aaaaa'])
135139

136140
@mock.patch('os.path.sep', new='/')
137141
def test_formatting_takes_just_last_part(self):
138-
self.assertEqual(autocomplete.FilenameCompletion.format('/hello/there/'), 'there/')
139-
self.assertEqual(autocomplete.FilenameCompletion.format('/hello/there'), 'there')
142+
self.assertEqual(self.completer.format('/hello/there/'), 'there/')
143+
self.assertEqual(self.completer.format('/hello/there'), 'there')
144+

0 commit comments

Comments
 (0)