iterative / shtab

↔️ Automagic shell tab completion for Python CLI applications
https://docs.iterative.ai/shtab
Other
362 stars 35 forks source link

use with pallets/click #138

Closed kameroncarverWPN closed 1 year ago

kameroncarverWPN commented 1 year ago

I have a highly nested click-based cli project with many imports, so the built-in click shell completion is unbearably slow (several seconds per tab complete). I would like to modify shtab to generate a bash completion file for it.

Looking at the code, I don't see any obvious stoppers to modifying get_bash_commands to pull the same information out of a click.Command or click.Group that is pulled out of a parser/subparser, but I admit I do not understand how the actual bash completion script works.

Have you looked at how click's Command/Group works before, and compared it to argparse-style parsers? Any reason why you think modifying shtab in this way wouldn't work? Any pointers for what I would need to do to make it work?

Thanks!

casperdcl commented 1 year ago

Hi! click does dynamic completions hence is quite slow, and is also not built-in (unlike argparse) so I don't use it much.

Would be happy to accept a PR though!

However I suspect the easiest thing to do is convert click objects to argparse.ArgumentParser (same idea as argopt, which converts docopt to argparse)... then tools like shtab will work without modification.

kameroncarverWPN commented 1 year ago

Your suggestion ended up being significantly easier than I was expecting! A hacky solution:

import click
import argparse

import shtab

@main.command(hidden=True)
@click.pass_context
def completion(ctx):
    """Generate bash completion script"""

    root = ctx.find_root()
    rootcmd = root.command

    main_parser = add_to_parser(rootcmd, argparse.ArgumentParser(prog=rootcmd.name))

    print(shtab.complete(main_parser))

def add_to_parser(command, parser):
    for param in command.params:
        if param.param_type_name == "option":
            spec = {}

            if param.is_flag:
                spec["action"] = "store_true"
            else:
                spec["nargs"] = param.nargs
                spec["required"] = param.required

            if isinstance(param.type, click.Choice):
                spec["choices"] = param.type.choices

            arg = parser.add_argument(*param.opts, **spec)

            if isinstance(param.type, click.File):
                arg.complete = shtab.FILE
            elif isinstance(param.type, click.Path):
                if param.type.file_okay and not param.type.dir_okay:
                    arg.complete = shtab.FILE
                elif not param.type.file_okay and param.type.dir_okay:
                    arg.complete = shtab.DIRECTORY

        elif param.param_type_name == "argument":
            spec = {}

            if param.nargs == -1:
                spec["nargs"] = "+" if param.required else "*"
            else:
                spec["nargs"] = param.nargs

            if isinstance(param.type, click.Choice):
                spec["choices"] = param.type.choices

            arg = parser.add_argument(param.name, **spec)

            if isinstance(param.type, click.File):
                arg.complete = shtab.FILE
            elif isinstance(param.type, click.Path):
                if param.type.file_okay and not param.type.dir_okay:
                    arg.complete = shtab.FILE
                elif not param.type.file_okay and param.type.dir_okay:
                    arg.complete = shtab.DIRECTORY

    if hasattr(command, "commands") and len(command.commands) > 0:
        subparsers = parser.add_subparsers()
        subparsers.required = True
        subparsers.dest = "subcommand"

        for name, subcmd in command.commands.items():
            if subcmd.hidden:
                continue

            # non-empty help necessary or argparse doesn't consider it an action??
            subparser = subparsers.add_parser(name, help=command.help)

            add_to_parser(subcmd, subparser)

    return parser

This is all I needed for my project, but this could expanded to take advantage of other shtab features.

Thanks for your great project!

casperdcl commented 1 year ago

Excellent! Were you thinking of releasing this as a small stand-alone package? Alternatively, if you'd prefer, I'd be happy to accept a PR creating shtab/contrib/click.py or similar :)