Skip to content

Commit f826598

Browse files
authored
Merge branch 'master' into transcript_tests
2 parents 6c33b58 + 92fdf41 commit f826598

File tree

5 files changed

+250
-12
lines changed

5 files changed

+250
-12
lines changed

cmd2/argcomplete_bridge.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
try:
55
# check if argcomplete is installed
66
import argcomplete
7-
except ImportError:
7+
except ImportError: # pragma: no cover
88
# not installed, skip the rest of the file
99
pass
1010

@@ -70,7 +70,7 @@ def tokens_for_completion(line, endidx):
7070
break
7171
except ValueError:
7272
# ValueError can be caused by missing closing quote
73-
if not quotes_to_try:
73+
if not quotes_to_try: # pragma: no cover
7474
# Since we have no more quotes to try, something else
7575
# is causing the parsing error. Return None since
7676
# this means the line is malformed.
@@ -228,15 +228,14 @@ def __call__(self, argument_parser, completer=None, always_complete_options=True
228228
output_stream.write(ifs.join(completions).encode(argcomplete.sys_encoding))
229229
elif outstr:
230230
# if there are no completions, but we got something from stdout, try to print help
231-
232231
# trick the bash completion into thinking there are 2 completions that are unlikely
233232
# to ever match.
234-
outstr = outstr.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').strip()
235-
# generate a filler entry that should always sort first
236-
filler = ' {0:><{width}}'.format('', width=len(outstr)/2)
237-
outstr = ifs.join([filler, outstr])
238233

239-
output_stream.write(outstr.encode(argcomplete.sys_encoding))
234+
comp_type = int(os.environ["COMP_TYPE"])
235+
if comp_type == 63: # type is 63 for second tab press
236+
print(outstr.rstrip(), file=argcomplete.debug_stream, end='')
237+
238+
output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
240239
else:
241240
# if completions is None we assume we don't know how to handle it so let bash
242241
# go forward with normal filesystem completion

cmd2/argparse_completer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,9 @@ def _match_argument(self, action, arg_strings_pattern):
877877

878878
return super(ACArgumentParser, self)._match_argument(action, arg_strings_pattern)
879879

880-
def _parse_known_args(self, arg_strings, namespace):
880+
# This is the official python implementation with a 5 year old patch applied
881+
# See the comment below describing the patch
882+
def _parse_known_args(self, arg_strings, namespace): # pragma: no cover
881883
# replace arg strings that are file references
882884
if self.fromfile_prefix_chars is not None:
883885
arg_strings = self._read_args_from_files(arg_strings)

examples/subcommands.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@
4141
from cmd2.argcomplete_bridge import CompletionFinder
4242
from cmd2.argparse_completer import AutoCompleter
4343
if __name__ == '__main__':
44-
with open('out.txt', 'a') as f:
45-
f.write('Here 1')
46-
f.flush()
4744
completer = CompletionFinder()
4845
completer(base_parser, AutoCompleter(base_parser))
4946
except ImportError:

tests/test_bashcompletion.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# coding=utf-8
2+
"""
3+
Unit/functional testing for argparse completer in cmd2
4+
5+
Copyright 2018 Eric Lin <[email protected]>
6+
Released under MIT license, see LICENSE file
7+
"""
8+
import os
9+
import pytest
10+
import sys
11+
from typing import List
12+
13+
from cmd2.argparse_completer import ACArgumentParser, AutoCompleter
14+
15+
16+
try:
17+
from cmd2.argcomplete_bridge import CompletionFinder
18+
skip_reason1 = False
19+
skip_reason = ''
20+
except ImportError:
21+
# Don't test if argcomplete isn't present (likely on Windows)
22+
skip_reason1 = True
23+
skip_reason = "argcomplete isn't installed\n"
24+
25+
skip_reason2 = "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true"
26+
if skip_reason2:
27+
skip_reason += 'These tests cannot run on TRAVIS\n'
28+
29+
skip_reason3 = sys.platform.startswith('win')
30+
if skip_reason3:
31+
skip_reason = 'argcomplete doesn\'t support Windows'
32+
33+
skip = skip_reason1 or skip_reason2 or skip_reason3
34+
35+
skip_mac = sys.platform.startswith('dar')
36+
37+
38+
actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
39+
'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac',
40+
'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
41+
'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee']
42+
43+
44+
def query_actors() -> List[str]:
45+
"""Simulating a function that queries and returns a completion values"""
46+
return actors
47+
48+
49+
@pytest.fixture
50+
def parser1():
51+
"""creates a argparse object to test completion against"""
52+
ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
53+
54+
def _do_media_movies(self, args) -> None:
55+
if not args.command:
56+
self.do_help('media movies')
57+
else:
58+
print('media movies ' + str(args.__dict__))
59+
60+
def _do_media_shows(self, args) -> None:
61+
if not args.command:
62+
self.do_help('media shows')
63+
64+
if not args.command:
65+
self.do_help('media shows')
66+
else:
67+
print('media shows ' + str(args.__dict__))
68+
69+
media_parser = ACArgumentParser(prog='media')
70+
71+
media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type')
72+
73+
movies_parser = media_types_subparsers.add_parser('movies')
74+
movies_parser.set_defaults(func=_do_media_movies)
75+
76+
movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command')
77+
78+
movies_list_parser = movies_commands_subparsers.add_parser('list')
79+
80+
movies_list_parser.add_argument('-t', '--title', help='Title Filter')
81+
movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
82+
choices=ratings_types)
83+
movies_list_parser.add_argument('-d', '--director', help='Director Filter')
84+
movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')
85+
86+
movies_add_parser = movies_commands_subparsers.add_parser('add')
87+
movies_add_parser.add_argument('title', help='Movie Title')
88+
movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
89+
movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True)
90+
movies_add_parser.add_argument('actor', help='Actors', nargs='*')
91+
92+
movies_commands_subparsers.add_parser('delete')
93+
94+
shows_parser = media_types_subparsers.add_parser('shows')
95+
shows_parser.set_defaults(func=_do_media_shows)
96+
97+
shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command')
98+
99+
shows_commands_subparsers.add_parser('list')
100+
101+
return media_parser
102+
103+
104+
# noinspection PyShadowingNames
105+
@pytest.mark.skipif(skip, reason=skip_reason)
106+
def test_bash_nocomplete(parser1):
107+
completer = CompletionFinder()
108+
result = completer(parser1, AutoCompleter(parser1))
109+
assert result is None
110+
111+
112+
# save the real os.fdopen
113+
os_fdopen = os.fdopen
114+
115+
116+
def my_fdopen(fd, mode, *args):
117+
"""mock fdopen that redirects 8 and 9 from argcomplete to stdin/stdout for testing"""
118+
if fd > 7:
119+
return os_fdopen(fd - 7, mode, *args)
120+
return os_fdopen(fd, mode)
121+
122+
123+
# noinspection PyShadowingNames
124+
@pytest.mark.skipif(skip, reason=skip_reason)
125+
def test_invalid_ifs(parser1, mock):
126+
completer = CompletionFinder()
127+
128+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
129+
'_ARGCOMPLETE_IFS': '\013\013'})
130+
131+
mock.patch.object(os, 'fdopen', my_fdopen)
132+
133+
with pytest.raises(SystemExit):
134+
completer(parser1, AutoCompleter(parser1), exit_method=sys.exit)
135+
136+
137+
# noinspection PyShadowingNames
138+
@pytest.mark.skipif(skip or skip_mac, reason=skip_reason)
139+
@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
140+
('media ', 'movies\013shows', ''),
141+
('media mo', 'movies', ''),
142+
('media movies add ', '\013\013 ', '''
143+
Hint:
144+
TITLE Movie Title'''),
145+
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
146+
('media movies list ', '', '')
147+
])
148+
def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
149+
completer = CompletionFinder()
150+
151+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
152+
'_ARGCOMPLETE_IFS': '\013',
153+
'COMP_TYPE': '63',
154+
'COMP_LINE': comp_line,
155+
'COMP_POINT': str(len(comp_line))})
156+
157+
mock.patch.object(os, 'fdopen', my_fdopen)
158+
159+
with pytest.raises(SystemExit):
160+
choices = {'actor': query_actors, # function
161+
}
162+
autocompleter = AutoCompleter(parser1, arg_choices=choices)
163+
completer(parser1, autocompleter, exit_method=sys.exit)
164+
165+
out, err = capfd.readouterr()
166+
assert out == exp_out
167+
assert err == exp_err
168+
169+
170+
def fdopen_fail_8(fd, mode, *args):
171+
"""mock fdopen that forces failure if fd == 8"""
172+
if fd == 8:
173+
raise IOError()
174+
return my_fdopen(fd, mode, *args)
175+
176+
177+
# noinspection PyShadowingNames
178+
@pytest.mark.skipif(skip, reason=skip_reason)
179+
def test_fail_alt_stdout(parser1, mock):
180+
completer = CompletionFinder()
181+
182+
comp_line = 'media movies list '
183+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
184+
'_ARGCOMPLETE_IFS': '\013',
185+
'COMP_TYPE': '63',
186+
'COMP_LINE': comp_line,
187+
'COMP_POINT': str(len(comp_line))})
188+
mock.patch.object(os, 'fdopen', fdopen_fail_8)
189+
190+
try:
191+
choices = {'actor': query_actors, # function
192+
}
193+
autocompleter = AutoCompleter(parser1, arg_choices=choices)
194+
completer(parser1, autocompleter, exit_method=sys.exit)
195+
except SystemExit as err:
196+
assert err.code == 1
197+
198+
199+
def fdopen_fail_9(fd, mode, *args):
200+
"""mock fdopen that forces failure if fd == 9"""
201+
if fd == 9:
202+
raise IOError()
203+
return my_fdopen(fd, mode, *args)
204+
205+
206+
# noinspection PyShadowingNames
207+
@pytest.mark.skipif(skip or skip_mac, reason=skip_reason)
208+
def test_fail_alt_stderr(parser1, capfd, mock):
209+
completer = CompletionFinder()
210+
211+
comp_line = 'media movies add '
212+
exp_out = '\013\013 '
213+
exp_err = '''
214+
Hint:
215+
TITLE Movie Title'''
216+
217+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
218+
'_ARGCOMPLETE_IFS': '\013',
219+
'COMP_TYPE': '63',
220+
'COMP_LINE': comp_line,
221+
'COMP_POINT': str(len(comp_line))})
222+
mock.patch.object(os, 'fdopen', fdopen_fail_9)
223+
224+
with pytest.raises(SystemExit):
225+
choices = {'actor': query_actors, # function
226+
}
227+
autocompleter = AutoCompleter(parser1, arg_choices=choices)
228+
completer(parser1, autocompleter, exit_method=sys.exit)
229+
230+
out, err = capfd.readouterr()
231+
assert out == exp_out
232+
assert err == exp_err

tox.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ deps =
1515
pyperclip
1616
pytest
1717
pytest-cov
18+
pytest-mock
19+
argcomplete
1820
wcwidth
1921
commands =
2022
py.test {posargs} --cov
@@ -25,6 +27,8 @@ deps =
2527
mock
2628
pyperclip
2729
pytest
30+
pytest-mock
31+
argcomplete
2832
wcwidth
2933
commands = py.test -v
3034

@@ -42,6 +46,8 @@ deps =
4246
pyperclip
4347
pytest
4448
pytest-cov
49+
pytest-mock
50+
argcomplete
4551
wcwidth
4652
commands =
4753
py.test {posargs} --cov
@@ -62,6 +68,8 @@ commands =
6268
deps =
6369
pyperclip
6470
pytest
71+
pytest-mock
72+
argcomplete
6573
wcwidth
6674
commands = py.test -v
6775

0 commit comments

Comments
 (0)