Skip to content

Commit 2a23ad8

Browse files
committed
Detect case where non-consecutive chars occur
Shouldn't happen with normal prompt_toolkit invocations, but still want to handle this gracefully.
1 parent 855f372 commit 2a23ad8

File tree

4 files changed

+65
-21
lines changed

4 files changed

+65
-21
lines changed

awsshell/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def main():
6363
index_data = load_index(index_file)
6464
doc_data = docs.load_doc_index(doc_index_file)
6565
completer = shellcomplete.AWSShellCompleter(
66-
autocomplete.AWSCLICompleter(index_data))
66+
autocomplete.AWSCLIModelCompleter(index_data))
6767
history = InMemoryHistory()
6868
shell = app.create_aws_shell(completer, history, doc_data)
6969
shell.run()

awsshell/autocomplete.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from awsshell.fuzzy import fuzzy_search
22

33

4-
class AWSCLICompleter(object):
4+
class AWSCLIModelCompleter(object):
5+
"""Autocompletion based on the JSON models for AWS services.
6+
7+
This class consumes indexed data based on the JSON models from
8+
AWS service (which we pull through botocore's data loaders).
9+
10+
"""
511
def __init__(self, index_data):
612
self._index = index_data
713
self._root_name = 'aws'
@@ -42,6 +48,8 @@ def autocomplete(self, line):
4248
# The user has hit backspace. We'll need to check
4349
# the current words.
4450
return self._handle_backspace()
51+
elif current_length != self._last_position + 1:
52+
return self._complete_from_full_parse()
4553

4654
# This position is important. We only update the _last_position
4755
# after we've checked the special cases above where that value
@@ -108,7 +116,7 @@ def _complete_from_full_parse(self):
108116
# can be optimized.
109117
self.reset()
110118
line = self._current_line
111-
for i in range(len(self._current_line)):
119+
for i in range(1, len(self._current_line)):
112120
self.autocomplete(line[:i])
113121
return self.autocomplete(line)
114122

awsshell/shellcomplete.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class AWSShellCompleter(Completer):
1616
"""Completer class for the aws-shell.
1717
1818
This is the completer used specifically for the aws shell.
19-
Not to be confused with the AWSCLICompleter, which is more
19+
Not to be confused with the AWSCLIModelCompleter, which is more
2020
low level, and can be reused in contexts other than the
2121
aws shell.
2222
"""
@@ -43,10 +43,12 @@ def get_completions(self, document, complete_event):
4343
text_before_cursor = document.text_before_cursor
4444
word_before_cursor = ''
4545
if text_before_cursor.strip():
46-
word_before_cursor = text_before_cursor.split()[-1]
46+
word_before_cursor = text_before_cursor.strip().split()[-1]
4747
completions = self._completer.autocomplete(text_before_cursor)
4848
arg_meta = self._completer.arg_metadata
4949
for completion in completions:
50+
# Go through the completions and add inline docs and
51+
# mark which options are required.
5052
if completion.startswith('--') and completion in arg_meta:
5153
# TODO: Need to handle merging in global options as well.
5254
meta = arg_meta[completion]

tests/test_completions.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from awsshell.autocomplete import AWSCLICompleter
2+
from awsshell.autocomplete import AWSCLIModelCompleter
33

44
@pytest.fixture
55
def index_data():
@@ -14,19 +14,19 @@ def index_data():
1414

1515
def test_completes_service_names(index_data):
1616
index_data['aws']['commands'] = ['first', 'second']
17-
completer = AWSCLICompleter(index_data)
17+
completer = AWSCLIModelCompleter(index_data)
1818
assert completer.autocomplete('fi') == ['first']
1919

2020

2121
def test_completes_multiple_service_names(index_data):
2222
index_data['aws']['commands'] = ['abc', 'acd', 'b']
23-
completer = AWSCLICompleter(index_data)
23+
completer = AWSCLIModelCompleter(index_data)
2424
assert completer.autocomplete('a') == ['abc', 'acd']
2525

2626

2727
def test_no_completion(index_data):
2828
index_data['aws']['commands'] = ['foo', 'bar']
29-
completer = AWSCLICompleter(index_data)
29+
completer = AWSCLIModelCompleter(index_data)
3030
assert completer.autocomplete('baz') == []
3131

3232

@@ -39,7 +39,7 @@ def test_can_complete_subcommands(index_data):
3939
'children': {},
4040
}
4141
}
42-
completer = AWSCLICompleter(index_data)
42+
completer = AWSCLIModelCompleter(index_data)
4343
# The completer tracks state to optimize lookups,
4444
# so we simulate exactly how it's called.
4545
completer.autocomplete('e')
@@ -61,7 +61,7 @@ def test_everything_completed_on_space(index_data):
6161
'children': {},
6262
}
6363
}
64-
completer = AWSCLICompleter(index_data)
64+
completer = AWSCLIModelCompleter(index_data)
6565
completer.autocomplete('e')
6666
completer.autocomplete('ec')
6767
completer.autocomplete('ec2')
@@ -71,13 +71,13 @@ def test_everything_completed_on_space(index_data):
7171

7272
def test_autocomplete_top_leve_services_on_space(index_data):
7373
index_data['aws']['commands'] = ['first', 'second']
74-
completer = AWSCLICompleter(index_data)
74+
completer = AWSCLIModelCompleter(index_data)
7575
assert completer.autocomplete(' ') == ['first', 'second']
7676

7777

7878
def test_reset_auto_complete(index_data):
7979
index_data['aws']['commands'] = ['first', 'second']
80-
completer = AWSCLICompleter(index_data)
80+
completer = AWSCLIModelCompleter(index_data)
8181
completer.autocomplete('f')
8282
completer.autocomplete('fi')
8383
completer.autocomplete('fir')
@@ -95,7 +95,7 @@ def test_reset_after_subcommand_completion(index_data):
9595
'children': {},
9696
}
9797
}
98-
completer = AWSCLICompleter(index_data)
98+
completer = AWSCLIModelCompleter(index_data)
9999
# The completer tracks state to optimize lookups,
100100
# so we simulate exactly how it's called.
101101
completer.autocomplete('e')
@@ -127,7 +127,7 @@ def test_can_handle_entire_line_deleted(index_data):
127127
'children': {},
128128
}
129129
}
130-
completer = AWSCLICompleter(index_data)
130+
completer = AWSCLIModelCompleter(index_data)
131131
c = completer.autocomplete
132132
c('e')
133133
c('ec')
@@ -145,7 +145,7 @@ def test_can_handle_entire_line_deleted(index_data):
145145

146146
def test_autocompletes_argument_names(index_data):
147147
index_data['aws']['arguments'] = ['--query', '--debug']
148-
completer = AWSCLICompleter(index_data)
148+
completer = AWSCLIModelCompleter(index_data)
149149
# These should only appear once in the output. So we need
150150
# to know if we're a top level argument or not.
151151
assert completer.autocomplete('-') == ['--query', '--debug']
@@ -162,7 +162,7 @@ def test_autocompletes_global_and_service_args(index_data):
162162
'children': {},
163163
}
164164
}
165-
completer = AWSCLICompleter(index_data)
165+
completer = AWSCLIModelCompleter(index_data)
166166
c = completer.autocomplete
167167
c('e')
168168
c('ec')
@@ -184,10 +184,10 @@ def test_can_mix_options_and_commands(index_data):
184184
'children': {},
185185
}
186186
}
187-
completer = AWSCLICompleter(index_data)
187+
completer = AWSCLIModelCompleter(index_data)
188188
c = completer.autocomplete
189189
partial_cmd = 'ec2 --no-validate-ssl'
190-
for i in range(len(partial_cmd)):
190+
for i in range(1, len(partial_cmd)):
191191
c(partial_cmd[:i])
192192

193193
assert c('ec2 --no-validate-ssl ') == ['create-tags', 'describe-instances']
@@ -209,12 +209,46 @@ def test_only_change_context_when_in_index(index_data):
209209
'arguments': [],
210210
}
211211
}
212-
completer = AWSCLICompleter(index_data)
212+
completer = AWSCLIModelCompleter(index_data)
213213
c = completer.autocomplete
214214
partial_cmd = 'ec2 --region us-west-2'
215-
for i in range(len(partial_cmd)):
215+
for i in range(1, len(partial_cmd)):
216216
c(partial_cmd[:i])
217217

218218
# We should ignore "us-west-2" because it's not a child
219219
# of ec2.
220220
assert c('ec2 --region us-west-2 ') == ['create-tags', 'describe-instances']
221+
222+
223+
def test_can_handle_skips_in_completion(index_data):
224+
# Normally, completion is always requested char by char.
225+
# Typing "ec2 describe-inst"
226+
# will subsequent calls to the autocompleter:
227+
# 'e', 'ec', 'ec2', 'ec2 ', 'ec2 d', 'ec2 de' ... all the way
228+
# up to 'ec2 describe-inst'.
229+
# However, the autocompleter should gracefully handle when there's
230+
# skips, so two subsequent calls are 'ec' and then 'ec2 describe-ta',
231+
# the autocompleter should still do the right thing. The tradeoff
232+
# will just be that this case will be slower than the common case
233+
# of char by char additions.
234+
index_data['aws']['commands'] = ['ec2']
235+
index_data['aws']['children'] = {
236+
'ec2': {
237+
'commands': ['create-tags', 'describe-instances'],
238+
'argument_metadata': {},
239+
'arguments': [],
240+
'children': {
241+
'create-tags': {
242+
'argument_metadata': {
243+
'--resources': {'example': '', 'minidoc': 'foo'},
244+
'--tags': {'example': 'bar', 'minidoc': 'baz'},
245+
},
246+
'arguments': ['--resources', '--tags'],
247+
}
248+
},
249+
}
250+
}
251+
completer = AWSCLIModelCompleter(index_data)
252+
c = completer.autocomplete
253+
result = c('ec2 create-ta')
254+
assert result == ['create-tags']

0 commit comments

Comments
 (0)