Skip to content

Commit

Permalink
Fix bash completion when defaults are present and the way subcommands…
Browse files Browse the repository at this point in the history
… are handled.

This makes the completion logic behave the same way the paser does.
Fixes pallets#925
Fixes pallets#919
  • Loading branch information
Nicholas Wiles committed Jun 27, 2018
1 parent cd90e83 commit 875c22f
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 46 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ Version 7.0
path so that changing working directories does not harm it.
- Force stdout/stderr writable. This works around issues with badly patched
standard streams like those from jupyter.
- Fix bug that caused bashcompletion to give improper completions on

.. _#866: https://github.com/pallets/click/issues/866
.. _#919: https://github.com/pallets/click/issues/919
.. _#925: https://github.com/pallets/click/issues/925
.. _#1027: https://github.com/pallets/click/pull/1027
.. _#1012: https://github.com/pallets/click/pull/1012
.. _#447: https://github.com/pallets/click/issues/447
Expand Down Expand Up @@ -193,7 +197,6 @@ Version 7.0
.. _#774: https://github.com/pallets/click/pull/774
.. _#790: https://github.com/pallets/click/pull/790


Version 6.8
-----------

Expand Down
46 changes: 28 additions & 18 deletions click/_bashcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,34 @@ def resolve_ctx(cli, prog_name, args):
:param args: full list of args
:return: the final context/command parsed
"""
ctx = cli.make_context(prog_name, args, resilient_parsing=True)
args_remaining = ctx.protected_args + ctx.args
while ctx is not None and args_remaining:
ctx = cli.make_context(prog_name, args, resilient_parsing=True, ignore_default_values=True)
args = ctx.protected_args + ctx.args
while args:
if isinstance(ctx.command, MultiCommand):
cmd = ctx.command.get_command(ctx, args_remaining[0])
if cmd is None:
return None
ctx = cmd.make_context(
args_remaining[0], args_remaining[1:], parent=ctx, resilient_parsing=True)
args_remaining = ctx.protected_args + ctx.args
if not ctx.command.chain:
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
if cmd is None:
return ctx
ctx = cmd.make_context(cmd_name, args, parent=ctx,
resilient_parsing=True,
ignore_default_values=True)
args = ctx.protected_args + ctx.args
else:
# Walk chained subcommand contexts saving the last one.
while args:
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
if cmd is None:
return ctx
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx,
allow_extra_args=True,
allow_interspersed_args=False,
resilient_parsing=True,
ignore_default_values=True)
args = sub_ctx.args
ctx = sub_ctx
args = sub_ctx.protected_args + sub_ctx.args
else:
ctx = ctx.parent

break
return ctx


Expand Down Expand Up @@ -216,12 +231,7 @@ def get_choices(cli, prog_name, args, incomplete):
# completion for argument values from user supplied values
for param in ctx.command.params:
if is_incomplete_argument(ctx.params, param):
completions.extend(get_user_autocompletions(
ctx, all_args, incomplete, param))
# Stop looking for other completions only if this argument is required.
if param.required:
return completions
break
return get_user_autocompletions(ctx, all_args, incomplete, param)

add_subcommand_completions(ctx, incomplete, completions)
return completions
Expand Down Expand Up @@ -252,4 +262,4 @@ def bashcomplete(cli, prog_name, complete_var, complete_instr):
return True
elif complete_instr == 'complete' or complete_instr == 'complete_zsh':
return do_complete(cli, prog_name, complete_instr == 'complete_zsh')
return False
return False
12 changes: 9 additions & 3 deletions click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def __init__(self, command, parent=None, info_name=None, obj=None,
resilient_parsing=False, allow_extra_args=None,
allow_interspersed_args=None,
ignore_unknown_options=None, help_option_names=None,
token_normalize_func=None, color=None):
token_normalize_func=None, color=None, ignore_default_values=False):
#: the parent context or `None` if none exists.
self.parent = parent
#: the :class:`Command` for this context.
Expand Down Expand Up @@ -315,6 +315,12 @@ def __init__(self, command, parent=None, info_name=None, obj=None,
#: will do its best to not cause any failures.
self.resilient_parsing = resilient_parsing

#: Indicates that default values should be ignored.
#: Useful for completion.

#: .. versionadded:: 7.0
self.ignore_default_values = ignore_default_values

# If there is no envvar prefix yet, but the parent has one and
# the command on this level has a name, we can expand the envvar
# prefix automatically.
Expand Down Expand Up @@ -1153,7 +1159,7 @@ def resolve_command(self, ctx, args):
# an option we want to kick off parsing again for arguments to
# resolve things like --help which now should go to the main
# place.
if cmd is None:
if cmd is None and not ctx.resilient_parsing:
if split_opt(cmd_name)[0]:
self.parse_args(ctx, ctx.args)
ctx.fail('No such command "%s".' % original_cmd_name)
Expand Down Expand Up @@ -1409,7 +1415,7 @@ def value_is_missing(self, value):
def full_process_value(self, ctx, value):
value = self.process_value(ctx, value)

if value is None:
if value is None and not ctx.ignore_default_values:
value = self.get_default(ctx)

if self.required and self.value_is_missing(value):
Expand Down
124 changes: 100 additions & 24 deletions tests/test_bashcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def csub(csub_opt, color):
def test_chaining():
@click.group('cli', chain=True)
@click.option('--cli-opt')
def cli(cli_opt):
@click.argument('arg', type=click.Choice(['cliarg1', 'cliarg2']))
def cli(cli_opt, arg):
pass

@cli.command()
Expand All @@ -142,33 +143,35 @@ def asub(asub_opt):

@cli.command(help='bsub help')
@click.option('--bsub-opt')
@click.argument('arg', type=click.Choice(['arg1', 'arg2']), required=True)
@click.argument('arg', type=click.Choice(['arg1', 'arg2']))
def bsub(bsub_opt, arg):
pass

@cli.command()
@click.option('--csub-opt')
@click.argument('arg', type=click.Choice(['carg1', 'carg2']), required=False)
@click.argument('arg', type=click.Choice(['carg1', 'carg2']), default='carg1')
def csub(csub_opt, arg):
pass

assert choices_without_help(cli, [], '-') == ['--cli-opt']
assert choices_without_help(cli, [], '') == ['asub', 'bsub', 'csub']
assert choices_without_help(cli, ['asub'], '-') == ['--asub-opt']
assert choices_without_help(cli, ['asub'], '') == ['bsub', 'csub']
assert choices_without_help(cli, ['bsub'], '') == ['arg1', 'arg2']
assert choices_without_help(cli, ['asub', '--asub-opt'], '') == []
assert choices_without_help(cli, ['asub', '--asub-opt', '5', 'bsub'], '-') == ['--bsub-opt']
assert choices_without_help(cli, ['asub', 'bsub'], '-') == ['--bsub-opt']
assert choices_with_help(cli, ['asub'], 'b') == [('bsub', 'bsub help')]
assert choices_without_help(cli, ['asub', 'csub'], '-') == ['--csub-opt']
assert choices_without_help(cli, [], '') == ['cliarg1', 'cliarg2']
assert choices_without_help(cli, ['cliarg1', 'asub'], '-') == ['--asub-opt']
assert choices_without_help(cli, ['cliarg1', 'asub'], '') == ['bsub', 'csub']
assert choices_without_help(cli, ['cliarg1', 'bsub'], '') == ['arg1', 'arg2']
assert choices_without_help(cli, ['cliarg1', 'asub', '--asub-opt'], '') == []
assert choices_without_help(cli, ['cliarg1', 'asub', '--asub-opt', '5', 'bsub'], '-') == ['--bsub-opt']
assert choices_without_help(cli, ['cliarg1', 'asub', 'bsub'], '-') == ['--bsub-opt']
assert choices_without_help(cli, ['cliarg1', 'asub', 'csub'], '') == ['carg1', 'carg2']
assert choices_without_help(cli, ['cliarg1', 'bsub', 'arg1', 'csub'], '') == ['carg1', 'carg2']
assert choices_without_help(cli, ['cliarg1', 'asub', 'csub'], '-') == ['--csub-opt']
assert choices_with_help(cli, ['cliarg1', 'asub'], 'b') == [('bsub', 'bsub help')]


def test_argument_choice():
@click.command()
@click.argument('arg1', required=False, type=click.Choice(['arg11', 'arg12']))
@click.argument('arg2', required=False, type=click.Choice(['arg21', 'arg22']))
@click.argument('arg3', required=False, type=click.Choice(['arg', 'argument']))
@click.argument('arg1', required=True, type=click.Choice(['arg11', 'arg12']))
@click.argument('arg2', type=click.Choice(['arg21', 'arg22']), default='arg21')
@click.argument('arg3', type=click.Choice(['arg', 'argument']), default='arg')
def cli():
pass

Expand All @@ -182,7 +185,7 @@ def cli():
def test_option_choice():
@click.command()
@click.option('--opt1', type=click.Choice(['opt11', 'opt12']), help='opt1 help')
@click.option('--opt2', type=click.Choice(['opt21', 'opt22']))
@click.option('--opt2', type=click.Choice(['opt21', 'opt22']), default='opt21')
@click.option('--opt3', type=click.Choice(['opt', 'option']))
def cli():
pass
Expand Down Expand Up @@ -218,6 +221,8 @@ def cli():
assert choices_without_help(cli, [''], '--opt1=') == ['opt11', 'opt12']
assert choices_without_help(cli, [], '') == ['arg11', 'arg12']
assert choices_without_help(cli, ['--opt2'], '') == ['opt21', 'opt22']
assert choices_without_help(cli, ['arg11'], '--opt') == ['--opt1', '--opt2']
assert choices_without_help(cli, [], '--opt') == ['--opt1', '--opt2']


def test_boolean_flag_choice():
Expand Down Expand Up @@ -258,11 +263,37 @@ def cli(local_opt):

def test_variadic_argument_choice():
@click.command()
@click.option('--opt', type=click.Choice(['opt1', 'opt2']))
@click.argument('src', nargs=-1, type=click.Choice(['src1', 'src2']))
def cli(local_opt):
pass

assert choices_without_help(cli, ['src1', 'src2'], '') == ['src1', 'src2']
assert choices_without_help(cli, ['src1', 'src2'], '--o') == ['--opt']
assert choices_without_help(cli, ['src1', 'src2', '--opt'], '') == ['opt1', 'opt2']
assert choices_without_help(cli, ['src1', 'src2'], '') == ['src1', 'src2']


def test_variadic_argument_complete():

def _complete(ctx, args, incomplete):
return ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz']

@click.group()
def entrypoint():
pass

@click.command()
@click.option('--opt', autocompletion=_complete)
@click.argument('arg', nargs=-1)
def subcommand(opt, arg):
pass

entrypoint.add_command(subcommand)

assert choices_without_help(entrypoint, ['subcommand', '--opt'], '') == _complete(0,0,0)
assert choices_without_help(entrypoint, ['subcommand', 'whatever', '--opt'], '') == _complete(0,0,0)
assert choices_without_help(entrypoint, ['subcommand', 'whatever', '--opt', 'abc'], '') == []


def test_long_chain_choice():
Expand All @@ -273,7 +304,7 @@ def cli():
@cli.group()
@click.option('--sub-opt', type=click.Choice(['subopt1', 'subopt2']))
@click.argument('sub-arg', required=False, type=click.Choice(['subarg1', 'subarg2']))
def sub(sub_opt):
def sub(sub_opt, sub_arg):
pass

@sub.command(short_help='bsub help')
Expand All @@ -283,15 +314,60 @@ def sub(sub_opt):
def bsub(bsub_opt):
pass

assert choices_with_help(cli, ['sub'], '') == [('subarg1', None), ('subarg2', None), ('bsub', 'bsub help')]
@sub.group('csub')
def csub():
pass

@csub.command()
def dsub():
pass

assert choices_with_help(cli, ['sub', 'subarg1'], '') == [('bsub', 'bsub help'), ('csub', '')]
assert choices_without_help(cli, ['sub'], '') == ['subarg1', 'subarg2']
assert choices_without_help(cli, ['sub', '--sub-opt'], '') == ['subopt1', 'subopt2']
assert choices_without_help(cli, ['sub', '--sub-opt', 'subopt1'], '') == \
['subarg1', 'subarg2', 'bsub']
['subarg1', 'subarg2']
assert choices_without_help(cli,
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '-') == ['--bsub-opt']
assert choices_without_help(cli,
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '') == ['bsubarg1', 'bsubarg2']
assert choices_without_help(cli,
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '-') == ['--bsub-opt']
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt'], '') == \
['bsubopt1', 'bsubopt2']
assert choices_without_help(cli,
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt'], '') == \
['bsubopt1', 'bsubopt2']
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt', 'bsubopt1', 'bsubarg1'],
'') == ['bbsubarg1', 'bbsubarg2']
assert choices_without_help(cli,
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt', 'bsubopt1', 'bsubarg1'],
'') == ['bbsubarg1', 'bbsubarg2']
['sub', '--sub-opt', 'subopt1', 'subarg1', 'csub'],
'') == ['dsub']


def test_chained_multi():
@click.group()
def cli():
pass

@cli.group()
def sub():
pass

@sub.group()
def bsub():
pass

@sub.group(chain=True)
def csub():
pass

@csub.command()
def dsub():
pass

@csub.command()
def esub():
pass

assert choices_without_help(cli, ['sub'], '') == ['bsub', 'csub']
assert choices_without_help(cli, ['sub'], 'c') == ['csub']
assert choices_without_help(cli, ['sub', 'csub'], '') == ['dsub', 'esub']
assert choices_without_help(cli, ['sub', 'csub', 'dsub'], '') == ['esub']

0 comments on commit 875c22f

Please sign in to comment.