2

I have a class that gets initialized with a previously unknown number of arguments and I want it to be done on CLI using Python's click package. My issue is that I can't manage to initialize it and run a click command:

$ python mycode.py arg1 arg2 ... argN click_command

Setting a defined number of arguments, like nargs=5, solves the issue of missing command but obligates me to input 5 arguments before my command. With variadic arguments like nargs=-1, click doesn't recognize click_command as a command.

How can I input n-many arguments, and then run the command using click?

import click

class Foo(object):
    def __init__(self, *args):
        self.args = args

    def log(self):
        print('self.args:', self.args)

pass_foo = click.make_pass_decorator(Foo)

@click.group()
@click.argument('myargs', nargs=-1)
@click.pass_context
def main(ctx, myargs):
    ctx.obj = Foo(myargs)
    print("arguments: ", myargs)

@main.command()
@pass_foo
def log(foo):
    foo.log()

main()

I expect to be able to run a click command after passing n-many args to my Foo() class, so I can initialize it and run its log() method as a CLI command, but the output is:

Error: Missing command

1 Answer 1

3

I am not entirely sure what you are trying to do is the best way to approach this problem. I would think that placing the variadic arguments after the command would be a bit more logical, and would definitely more align with the way click works. But, you can do what you are after with this:

Custom Class:

class CommandAfterArgs(click.Group):

    def parse_args(self, ctx, args):
        parsed_args = super(CommandAfterArgs, self).parse_args(ctx, args)
        possible_command = ctx.params['myargs'][-1]
        if possible_command in self.commands:
            ctx.protected_args = [possible_command]
            ctx.params['myargs'] = ctx.params['myargs'][:-1]

        elif possible_command in ('-h', '--help'):
            if len(ctx.params['myargs']) > 1 and \
                    ctx.params['myargs'][-2] in self.commands:
                ctx.protected_args = [ctx.params['myargs'][-2]]
                parsed_args = ['--help']
                ctx.params['myargs'] = ctx.params['myargs'][:-2]
                ctx.args = [possible_command]

        return parsed_args

Using Custom Class:

Then to use the custom class, pass it as the cls argument to the group decorator like:

@click.group(cls=CommandAfterArgs)
@click.argument('myargs', nargs=-1)
def main(myargs):
    ...

Test Code:

import click

class Foo(object):
    def __init__(self, *args):
        self.args = args

    def log(self):
        print('self.args:', self.args)


pass_foo = click.make_pass_decorator(Foo)


@click.group(cls=CommandAfterArgs)
@click.argument('myargs', nargs=-1)
@click.pass_context
def main(ctx, myargs):
    ctx.obj = Foo(*myargs)
    print("arguments: ", myargs)


@main.command()
@pass_foo
def log(foo):
    foo.log()


if __name__ == "__main__":
    commands = (
        'arg1 arg2 log',
        'log --help',
        '--help',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            main(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> arg1 arg2 log
arguments:  ('arg1', 'arg2')
self.args: ('arg1', 'arg2')
-----------
> log --help
arguments:  ()
Usage: test.py log [OPTIONS]

Options:
  --help  Show this message and exit.
-----------
> --help
Usage: test.py [OPTIONS] [MYARGS]... COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  log
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for your very nice answer, @stephen. If I place the variadic arguments after subcommands, aligning with the way click works as you are suggesting, I won't be able to initialize Foo() class on main() group command, in order to create an instance of Foo() to be used by its subcommands. The only way I have figured out how to run a subcommand followed by args at this Foo(*args) scenario was to instantiate it inside every subcommand. So is this the right way to do it? Is it useless trying to apply click on a class like Foo(*args)?
I believe your use case can be handled as you desire. Suggest generating another question, with that as the specific problem you are trying to solve via click. Also I vaguely remember answering something similar already here on SO. maybe here
@StephenRauch you have done a similar answer indeed and with that I could answer my question by myself. Thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.