Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions cmd2/argcomplete_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
try:
# check if argcomplete is installed
import argcomplete
except ImportError:
except ImportError: # pragma: no cover
# not installed, skip the rest of the file
pass

Expand Down Expand Up @@ -70,7 +70,7 @@ def tokens_for_completion(line, endidx):
break
except ValueError:
# ValueError can be caused by missing closing quote
if not quotes_to_try:
if not quotes_to_try: # pragma: no cover
# Since we have no more quotes to try, something else
# is causing the parsing error. Return None since
# this means the line is malformed.
Expand Down Expand Up @@ -228,15 +228,14 @@ def __call__(self, argument_parser, completer=None, always_complete_options=True
output_stream.write(ifs.join(completions).encode(argcomplete.sys_encoding))
elif outstr:
# if there are no completions, but we got something from stdout, try to print help

# trick the bash completion into thinking there are 2 completions that are unlikely
# to ever match.
outstr = outstr.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').strip()
# generate a filler entry that should always sort first
filler = ' {0:><{width}}'.format('', width=len(outstr)/2)
outstr = ifs.join([filler, outstr])

output_stream.write(outstr.encode(argcomplete.sys_encoding))
comp_type = int(os.environ["COMP_TYPE"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange. But if it works, cool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. The first tab press comes in as 9. Makes total sense.

if comp_type == 63: # type is 63 for second tab press
print(outstr.rstrip(), file=argcomplete.debug_stream, end='')

output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
else:
# if completions is None we assume we don't know how to handle it so let bash
# go forward with normal filesystem completion
Expand Down
4 changes: 3 additions & 1 deletion cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,9 @@ def _match_argument(self, action, arg_strings_pattern):

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

def _parse_known_args(self, arg_strings, namespace):
# This is the official python implementation with a 5 year old patch applied
# See the comment below describing the patch
def _parse_known_args(self, arg_strings, namespace): # pragma: no cover
# replace arg strings that are file references
if self.fromfile_prefix_chars is not None:
arg_strings = self._read_args_from_files(arg_strings)
Expand Down
3 changes: 0 additions & 3 deletions examples/subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@
from cmd2.argcomplete_bridge import CompletionFinder
from cmd2.argparse_completer import AutoCompleter
if __name__ == '__main__':
with open('out.txt', 'a') as f:
f.write('Here 1')
f.flush()
completer = CompletionFinder()
completer(base_parser, AutoCompleter(base_parser))
except ImportError:
Expand Down
232 changes: 232 additions & 0 deletions tests/test_bashcompletion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# coding=utf-8
"""
Unit/functional testing for argparse completer in cmd2

Copyright 2018 Eric Lin <anselor@gmail.com>
Released under MIT license, see LICENSE file
"""
import os
import pytest
import sys
from typing import List

from cmd2.argparse_completer import ACArgumentParser, AutoCompleter


try:
from cmd2.argcomplete_bridge import CompletionFinder
skip_reason1 = False
skip_reason = ''
except ImportError:
# Don't test if argcomplete isn't present (likely on Windows)
skip_reason1 = True
skip_reason = "argcomplete isn't installed\n"

skip_reason2 = "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true"
if skip_reason2:
skip_reason += 'These tests cannot run on TRAVIS\n'

skip_reason3 = sys.platform.startswith('win')
if skip_reason3:
skip_reason = 'argcomplete doesn\'t support Windows'

skip = skip_reason1 or skip_reason2 or skip_reason3

skip_mac = sys.platform.startswith('dar')


actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac',
'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee']


def query_actors() -> List[str]:
"""Simulating a function that queries and returns a completion values"""
return actors


@pytest.fixture
def parser1():
"""creates a argparse object to test completion against"""
ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']

def _do_media_movies(self, args) -> None:
if not args.command:
self.do_help('media movies')
else:
print('media movies ' + str(args.__dict__))

def _do_media_shows(self, args) -> None:
if not args.command:
self.do_help('media shows')

if not args.command:
self.do_help('media shows')
else:
print('media shows ' + str(args.__dict__))

media_parser = ACArgumentParser(prog='media')

media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type')

movies_parser = media_types_subparsers.add_parser('movies')
movies_parser.set_defaults(func=_do_media_movies)

movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command')

movies_list_parser = movies_commands_subparsers.add_parser('list')

movies_list_parser.add_argument('-t', '--title', help='Title Filter')
movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
choices=ratings_types)
movies_list_parser.add_argument('-d', '--director', help='Director Filter')
movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')

movies_add_parser = movies_commands_subparsers.add_parser('add')
movies_add_parser.add_argument('title', help='Movie Title')
movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True)
movies_add_parser.add_argument('actor', help='Actors', nargs='*')

movies_commands_subparsers.add_parser('delete')

shows_parser = media_types_subparsers.add_parser('shows')
shows_parser.set_defaults(func=_do_media_shows)

shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command')

shows_commands_subparsers.add_parser('list')

return media_parser


# noinspection PyShadowingNames
@pytest.mark.skipif(skip, reason=skip_reason)
def test_bash_nocomplete(parser1):
completer = CompletionFinder()
result = completer(parser1, AutoCompleter(parser1))
assert result is None


# save the real os.fdopen
os_fdopen = os.fdopen


def my_fdopen(fd, mode, *args):
"""mock fdopen that redirects 8 and 9 from argcomplete to stdin/stdout for testing"""
if fd > 7:
return os_fdopen(fd - 7, mode, *args)
return os_fdopen(fd, mode)


# noinspection PyShadowingNames
@pytest.mark.skipif(skip, reason=skip_reason)
def test_invalid_ifs(parser1, mock):
completer = CompletionFinder()

mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
'_ARGCOMPLETE_IFS': '\013\013'})

mock.patch.object(os, 'fdopen', my_fdopen)

with pytest.raises(SystemExit):
completer(parser1, AutoCompleter(parser1), exit_method=sys.exit)


# noinspection PyShadowingNames
@pytest.mark.skipif(skip or skip_mac, reason=skip_reason)
@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
('media ', 'movies\013shows', ''),
('media mo', 'movies', ''),
('media movies add ', '\013\013 ', '''
Hint:
TITLE Movie Title'''),
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
('media movies list ', '', '')
])
def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
completer = CompletionFinder()

mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
'_ARGCOMPLETE_IFS': '\013',
'COMP_TYPE': '63',
'COMP_LINE': comp_line,
'COMP_POINT': str(len(comp_line))})

mock.patch.object(os, 'fdopen', my_fdopen)

with pytest.raises(SystemExit):
choices = {'actor': query_actors, # function
}
autocompleter = AutoCompleter(parser1, arg_choices=choices)
completer(parser1, autocompleter, exit_method=sys.exit)

out, err = capfd.readouterr()
assert out == exp_out
assert err == exp_err


def fdopen_fail_8(fd, mode, *args):
"""mock fdopen that forces failure if fd == 8"""
if fd == 8:
raise IOError()
return my_fdopen(fd, mode, *args)


# noinspection PyShadowingNames
@pytest.mark.skipif(skip, reason=skip_reason)
def test_fail_alt_stdout(parser1, mock):
completer = CompletionFinder()

comp_line = 'media movies list '
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
'_ARGCOMPLETE_IFS': '\013',
'COMP_TYPE': '63',
'COMP_LINE': comp_line,
'COMP_POINT': str(len(comp_line))})
mock.patch.object(os, 'fdopen', fdopen_fail_8)

try:
choices = {'actor': query_actors, # function
}
autocompleter = AutoCompleter(parser1, arg_choices=choices)
completer(parser1, autocompleter, exit_method=sys.exit)
except SystemExit as err:
assert err.code == 1


def fdopen_fail_9(fd, mode, *args):
"""mock fdopen that forces failure if fd == 9"""
if fd == 9:
raise IOError()
return my_fdopen(fd, mode, *args)


# noinspection PyShadowingNames
@pytest.mark.skipif(skip or skip_mac, reason=skip_reason)
def test_fail_alt_stderr(parser1, capfd, mock):
completer = CompletionFinder()

comp_line = 'media movies add '
exp_out = '\013\013 '
exp_err = '''
Hint:
TITLE Movie Title'''

mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
'_ARGCOMPLETE_IFS': '\013',
'COMP_TYPE': '63',
'COMP_LINE': comp_line,
'COMP_POINT': str(len(comp_line))})
mock.patch.object(os, 'fdopen', fdopen_fail_9)

with pytest.raises(SystemExit):
choices = {'actor': query_actors, # function
}
autocompleter = AutoCompleter(parser1, arg_choices=choices)
completer(parser1, autocompleter, exit_method=sys.exit)

out, err = capfd.readouterr()
assert out == exp_out
assert err == exp_err
8 changes: 8 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ deps =
pyperclip
pytest
pytest-cov
pytest-mock
argcomplete
wcwidth
commands =
py.test {posargs} --cov
Expand All @@ -25,6 +27,8 @@ deps =
mock
pyperclip
pytest
pytest-mock
argcomplete
wcwidth
commands = py.test -v

Expand All @@ -42,6 +46,8 @@ deps =
pyperclip
pytest
pytest-cov
pytest-mock
argcomplete
wcwidth
commands =
py.test {posargs} --cov
Expand All @@ -62,6 +68,8 @@ commands =
deps =
pyperclip
pytest
pytest-mock
argcomplete
wcwidth
commands = py.test -v