Python Click autocomplete for (str, str) option
Asked Answered
O

1

7

I am writing a CLI tool with Python and Click. One of the commands has an option of type=(str, str)) which is used like this: command subcommand --option foo bar.

There are several options to choose for the first argument (foo) and for each choice of the first argument, there are several options to choose for the second one (bar). So the options for the second argument depend on the choice of the first one!

The question is: how can I use the support that Click provides to write an autocompletion for that?

In particular:

  • do I need two completion functions, one for each argument?
  • And how is the second function provided with the choice of the first argument?
  • How to define two completion functions for a single option?
  • Or, can I use one function for both args? What would that look like?
Ogata answered 27/10, 2019 at 8:44 Comment(3)
do you mean autocompletion in a shell? (which is something you need to get your shell to do - not something you can do in your python program...)...Gamp
Yes, I mean shell autocompletion. And for simpler cases click makes it easy to integrate it: click.palletsprojects.com/en/7.x/bashcomplete I was hoping that my complex case can also be solved in a similar way.Ogata
oh, did not know that... thanks!Gamp
E
10

To autocomplete a click option which is two strings, in which the second string depends on the first, you do not need two completion functions. You simply need a way to determine which of the two strings is currently being completed. For an option named --opt we can complete a type of (str, str) like:

Code:

def get_opts(ctx, args, incomplete):
    """ auto complete for option "opt"

    :param ctx: The current click context.
    :param args: The list of arguments passed in.
    :param incomplete: The partial word that is being completed, as a
        string. May be an empty string '' if no characters have
        been entered yet.
    :return: list of possible choices
    """
    opts = {
        'foo1': ('bar11', 'bar21', 'bar31'),
        'foo2': ('bar12', 'bar22', 'bar32'),
        'fox3': ('bar13', 'bar23', 'bar33'),
    }
    if args[-1] == '--opt':
        possible_choices = opts.keys()
    elif args[-1] in opts:
        possible_choices = opts[args[-1]]
    else:
        possible_choices = ()
    return [arg for arg in possible_choices if arg.startswith(incomplete)]

Using autocompletion

You can pass the autocompletion function to click like:

@click.option('--opt', type=(str, str), autocompletion=get_opts)

How does this work?

The autocompletion function is passed a list of args. When completing an option, we can find our option name in the args. In this case we can look for --opt in the args to get an anchor for the position of whether we are completing the first or second string. Then we return the string which matches the already entered character.

Test Code:

import click

@click.command()
@click.option('--opt', type=(str, str), autocompletion=get_opts)
@click.argument('arg')
def cli(opt, arg):
    """My Great Cli"""

if __name__ == "__main__":
    commands = (
        ('--opt', 2, 'foo1 foo2 fox3'),
        ('--opt f', 2, 'foo1 foo2 fox3'),
        ('--opt fo', 2, 'foo1 foo2 fox3'),
        ('--opt foo', 2, 'foo1 foo2'),
        ('--opt fox', 2, 'fox3'),
        ('--opt foz', 2, ''),
        ('--opt foo2 b', 3, 'bar12 bar22 bar32'),
        ('--opt foo2 bar1', 3, 'bar12'),
        ('--opt foo2 baz', 3, ''),
    )

    import os
    import sys
    from unittest import mock
    from click._bashcomplete import do_complete

    failed = []
    for cmd_args in commands:
        cmd_args_with_arg = (
            'arg ' + cmd_args[0], cmd_args[1] + 1, cmd_args[2])
        for cmd in (cmd_args, cmd_args_with_arg):
            with mock.patch('click._bashcomplete.echo') as echo:
                os.environ['COMP_WORDS'] = 'x ' + cmd[0]
                os.environ['COMP_CWORD'] = str(cmd[1])
                do_complete(cli, 'x', False)
                completions = [c[0][0] for c in echo.call_args_list]
                if completions != cmd[2].split():
                    failed.append(completions, cmd[2].split())

    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    if failed:
        for fail in failed:
            print('Got {}, expected {}'.format(completions, cmd[2].split()))
    else:
        print('All tests passed')

Test Results:

Click Version: 7.0
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
All tests passed
Ethereal answered 30/10, 2019 at 0:17 Comment(1)
This works beautifully, thanks! Also bonus thanks for an example of how to unit-test autocompletion.Ogata

© 2022 - 2024 — McMap. All rights reserved.