How can I achieve the following synopsis using the Python click library?
Usage: app CMD [OPTIONS] [FOO] [BAR]
app CMD [OPTIONS] [FOOBAR]
I can't figure out whether I am able to pass two different sets of named argument for the same command based on the number of given arguments. That is, if only one argument was passed it's foobar, but if two arguments were passed, they are foo and bar.
The code representation of such implementation would look something like this (provided you could use function overload, which you can't)
#click.command()
#click.argument('foo', required=False)
#click.argument('bar', required=False)
def cmd(foo, bar):
# ...
#click.command()
#click.argument('foobar', required=False)
def cmd(foobar):
# ...
You can add multiple command handlers with a different number of arguments for each by creating a custom click.Command class. There is some ambiguity around which of the command handlers would best be called if parameters are not strictly required, but that can be mostly dealt with by using the first signature that fits the command line passed.
Custom Class
class AlternateArgListCmd(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.alternate_arglist_handlers = [(self, super())]
self.alternate_self = self
def alternate_arglist(self, *args, **kwargs):
from click.decorators import command as cmd_decorator
def decorator(f):
command = cmd_decorator(*args, **kwargs)(f)
self.alternate_arglist_handlers.append((command, command))
# verify we have no options defined and then copy options from base command
options = [o for o in command.params if isinstance(o, click.Option)]
if options:
raise click.ClickException(
f'Options not allowed on {type(self).__name__}: {[o.name for o in options]}')
command.params.extend(o for o in self.params if isinstance(o, click.Option))
return command
return decorator
def make_context(self, info_name, args, parent=None, **extra):
"""Attempt to build a context for each variant, use the first that succeeds"""
orig_args = list(args)
for handler, handler_super in self.alternate_arglist_handlers:
args[:] = list(orig_args)
self.alternate_self = handler
try:
return handler_super.make_context(info_name, args, parent, **extra)
except click.UsageError:
pass
except:
raise
# if all alternates fail, return the error message for the first command defined
args[:] = orig_args
return super().make_context(info_name, args, parent, **extra)
def invoke(self, ctx):
"""Use the callback for the appropriate variant"""
if self.alternate_self.callback is not None:
return ctx.invoke(self.alternate_self.callback, **ctx.params)
return super().invoke(ctx)
def format_usage(self, ctx, formatter):
"""Build a Usage for each variant"""
prefix = "Usage: "
for _, handler_super in self.alternate_arglist_handlers:
pieces = handler_super.collect_usage_pieces(ctx)
formatter.write_usage(ctx.command_path, " ".join(pieces), prefix=prefix)
prefix = " " * len(prefix)
Using the Custom Class:
To use the custom class, pass it as the cls argument to the click.command decorator like:
#click.command(cls=AlternateArgListCmd)
#click.argument('foo')
#click.argument('bar')
def cli(foo, bar):
...
Then use the alternate_arglist() decorator on the command to add another
command handler with different arguments.
#cli.alternate_arglist()
#click.argument('foobar')
def cli_one_param(foobar):
...
How does this work?
This works because click is a well designed OO framework. The #click.command() decorator
usually instantiates a click.Command object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command in our own class and over ride the desired methods.
In this case we add a new decorator method: alternate_arglist(), and override three methods: make_context(), invoke() & format_usage(). The overridden make_context() method checks to see which of the command handler variants matches the number of args passed, the overridden invoke() method is used to call the appropriate command handler variant and the overridden format_usage() is used to create the help message showing the various usages.
Test Code:
import click
#click.command(cls=AlternateArgListCmd)
#click.argument('foo')
#click.argument('bar')
#click.argument('baz')
#click.argument('bing', required=False)
#click.option('--an-option', default='empty')
def cli(foo, bar, baz, bing, an_option):
"""Best Command Ever!"""
if bing is not None:
click.echo(f'foo bar baz bing an-option: {foo} {bar} {baz} {bing} {an_option}')
else:
click.echo(f'foo bar baz an-option: {foo} {bar} {baz} {an_option}')
#cli.alternate_arglist()
#click.argument('foo')
#click.argument('bar')
def cli_two_param(foo, bar, an_option):
click.echo(f'foo bar an-option: {foo} {bar} {an_option}')
#cli.alternate_arglist()
#click.argument('foobar', required=False)
def cli_one_param(foobar, an_option):
click.echo(f'foobar an-option: {foobar} {an_option}')
if __name__ == "__main__":
commands = (
'',
'p1',
'p1 p2 --an-option=optional',
'p1 p2 p3',
'p1 p2 p3 p4 --an-option=optional',
'p1 p2 p3 p4 p5',
'--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)
cli(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Test Results:
Click Version: 7.1.2
Python Version: 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)]
-----------
>
foobar an-option: None empty
-----------
> p1
foobar an-option: p1 empty
-----------
> p1 p2 --an-option=optional
foo bar an-option: p1 p2 optional
-----------
> p1 p2 p3
foo bar baz an-option: p1 p2 p3 empty
-----------
> p1 p2 p3 p4 --an-option=optional
foo bar baz bing an-option: p1 p2 p3 p4 optional
-----------
> p1 p2 p3 p4 p5
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
test_code.py [OPTIONS] FOO BAR
test_code.py [OPTIONS] [FOOBAR]
Try 'test_code.py --help' for help.
Error: Got unexpected extra argument (p5)
-----------
> --help
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
test_code.py [OPTIONS] FOO BAR
test_code.py [OPTIONS] [FOOBAR]
Best Command Ever!
Options:
--an-option TEXT
--help Show this message and exit.
Related
Let's say I have a function f1()
def f1(input1, input2):
# does some magic
return p1, p2
if __name__ == "__main__":
parser = ArgumentParser(description='Help function description')
parser.add_argument('t1', help='Token')
parser.add_argument('t2', help='Another token')
args = parser.parse_args()
p1, p2 = f1(args.t1, args.t2)
Let's say this file is called street.py. I want to use the same file to return p1 when the argument arg1 is an input and p2 when argument arg2 is an input.
Outside the file :
variable1 = (python3 ../street.py t1 t2 -arg1)
variable2 = (python3 ../street.py t1 t2 -arg2)
I'm a bit confused over the argparse argument style from the documentation, what's the easiest way to do this?
You can solve your problem by using:
parser.add_argument('-arg1', '--argument1', action='store_true')
args = parser.parse_args()
this will store True in the variable args.argument1 if you call python3 ../street.py t1 t2 -arg1, so that you can later use a simple if/elif statement to separate the different cases, i.e.:
if args.argument1 is True:
print(p1)
TL;DR
Make the call in the terminal like this (linux)
variable=$(python3 streep.py t1 'foo' t2 'bar')
you should associate each param t1 and t2 with their respective values.
Also only return a single value from f1 which you will print to stdout
You should consider what you want the behaviour to be. If you want to use the output of f1 then be explicit in its arguments what is mandatory and what is an optional input.
Also consider what you want the output to be, since this is a script passing values will be through stdout you kinda only have the one line to print.
def f1(input1, input2):
# does some magic
return p1 or p2
Then simply print the result to stdout with python print() statement
An example based on your code could look like
from argparse import ArgumentParser
import sys
def f1(input1, input2):
# does some magic
return input1 or input2
if __name__ == "__main__":
parser = ArgumentParser(description='Help function description')
parser.add_argument('--t1', help='Token')
parser.add_argument('--t2', help='Another token')
args = parser.parse_args()
try:
result = f1(args.t1, args.t2)
print(result)
except Exception as e:
print(e, file=sys.stderr)
exit(1)
I can then use this script as
variable=$(python3 streep.py --t2 'bar')
With this file:
import argparse, sys
def f1(input1, input2):
# does some magic
print('inside f1')
return input1, input2
if __name__ == "__main__":
print(sys.argv)
parser = argparse.ArgumentParser(description='Help function description')
parser.add_argument('t1', help='Token')
parser.add_argument('t2', help='Another token')
args = parser.parse_args()
print(args)
p1, p2 = f1(args.t1, args.t2)
print(p1, p2)
Asking for help (in a shell window):
201:~/mypy$ python3 stack64684099.py -h foo bar
['stack64684099.py', '-h', 'foo', 'bar']
usage: stack64684099.py [-h] t1 t2
Help function description
positional arguments:
t1 Token
t2 Another token
optional arguments:
-h, --help show this help message and exit
running with 2 arguments as specified in the help:
1201:~/mypy$ python3 stack64684099.py foo bar
['stack64684099.py', 'foo', 'bar']
Namespace(t1='foo', t2='bar')
inside f1
foo bar
I added prints to show sys.argv the list of strings that argparse gets from the shell. And the namespace that parsing produced, and the results of running f1.
===
I don't know where you are doing:
variable1 = (python3 ../street.py t1 t2 -arg1)
variable2 = (python3 ../street.py t1 t2 -arg2)
There's nothing in your parser set up that looks for or pays attention to those '-arg1','-arg2' strings. And what they are supposed to do is not clear in your question.
I could redirect stdout to file:
1202:~/mypy$ python3 stack64684099.py foo bar > test.txt
1208:~/mypy$ cat test.txt
['stack64684099.py', 'foo', 'bar']
Namespace(t1='foo', t2='bar')
inside f1
foo bar
Omitting all but the last print give just the foo bar line.
The primary purpose of the parser is to read and understand/parse the user input. If you are happy with the results in the args namespace, the rest is up to your own code. As defined your arguments are simple strings, which you can use as is or test for certain values. You can also produce numbers, and with the correct action, boolean values. You can also define optionals, arguments that expect a flag string like --arg1.
Is there an equivalent to argparse's nargs='*' functionality for optional arguments in Click?
I am writing a command line script, and one of the options needs to be able to take an unlimited number of arguments, like:
foo --users alice bob charlie --bar baz
So users would be ['alice', 'bob', 'charlie'] and bar would be 'baz'.
In argparse, I can specify multiple optional arguments to collect all of the arguments that follow them by setting nargs='*'.
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--users', nargs='*')
>>> parser.add_argument('--bar')
>>> parser.parse_args('--users alice bob charlie --bar baz'.split())
Namespace(bar='baz', users=['alice', 'bob', 'charlie'])
I know Click allows you to specify an argument to accept unlimited inputs by setting nargs=-1, but when I try to set an optional argument's nargs to -1, I get:
TypeError: Options cannot have nargs < 0
Is there a way to make Click accept an unspecified number of arguments for an option?
Update:
I need to be able to specify options after the option that takes unlimited arguments.
Update:
#Stephen Rauch's answer answers this question. However, I don't recommend using the approach I ask for here. My feature request is intentionally not implemented in Click, since it can result in unexpected behaviors. Click's recommended approach is to use multiple=True:
#click.option('-u', '--user', 'users', multiple=True)
And in the command line, it will look like:
foo -u alice -u bob -u charlie --bar baz
One way to approach what you are after is to inherit from click.Option, and customize the parser.
Custom Class:
import click
class OptionEatAll(click.Option):
def __init__(self, *args, **kwargs):
self.save_other_options = kwargs.pop('save_other_options', True)
nargs = kwargs.pop('nargs', -1)
assert nargs == -1, 'nargs, if set, must be -1 not {}'.format(nargs)
super(OptionEatAll, self).__init__(*args, **kwargs)
self._previous_parser_process = None
self._eat_all_parser = None
def add_to_parser(self, parser, ctx):
def parser_process(value, state):
# method to hook to the parser.process
done = False
value = [value]
if self.save_other_options:
# grab everything up to the next option
while state.rargs and not done:
for prefix in self._eat_all_parser.prefixes:
if state.rargs[0].startswith(prefix):
done = True
if not done:
value.append(state.rargs.pop(0))
else:
# grab everything remaining
value += state.rargs
state.rargs[:] = []
value = tuple(value)
# call the actual process
self._previous_parser_process(value, state)
retval = super(OptionEatAll, self).add_to_parser(parser, ctx)
for name in self.opts:
our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
if our_parser:
self._eat_all_parser = our_parser
self._previous_parser_process = our_parser.process
our_parser.process = parser_process
break
return retval
Using Custom Class:
To use the custom class, pass the cls parameter to #click.option() decorator like:
#click.option("--an_option", cls=OptionEatAll)
or if it is desired that the option will eat the entire rest of the command line, not respecting other options:
#click.option("--an_option", cls=OptionEatAll, save_other_options=False)
How does this work?
This works because click is a well designed OO framework. The #click.option() decorator usually instantiates a
click.Option object but allows this behavior to be over ridden with the cls parameter. So it is a relatively
easy matter to inherit from click.Option in our own class and over ride the desired methods.
In this case we over ride click.Option.add_to_parser() and the monkey patch the parser so that we can
eat more than one token if desired.
Test Code:
#click.command()
#click.option('-g', 'greedy', cls=OptionEatAll, save_other_options=False)
#click.option('--polite', cls=OptionEatAll)
#click.option('--other')
def foo(polite, greedy, other):
click.echo('greedy: {}'.format(greedy))
click.echo('polite: {}'.format(polite))
click.echo('other: {}'.format(other))
if __name__ == "__main__":
commands = (
'-g a b --polite x',
'-g a --polite x y --other o',
'--polite x y --other o',
'--polite x -g a b c --other o',
'--polite x --other o -g a b c',
'-g a b c',
'-g a',
'-g',
'extra',
'--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)
foo(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Test 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)]
-----------
> -g a b --polite x
greedy: ('a', 'b', '--polite', 'x')
polite: None
other: None
-----------
> -g a --polite x y --other o
greedy: ('a', '--polite', 'x', 'y', '--other', 'o')
polite: None
other: None
-----------
> --polite x y --other o
greedy: None
polite: ('x', 'y')
other: o
-----------
> --polite x -g a b c --other o
greedy: ('a', 'b', 'c', '--other', 'o')
polite: ('x',)
other: None
-----------
> --polite x --other o -g a b c
greedy: ('a', 'b', 'c')
polite: ('x',)
other: o
-----------
> -g a b c
greedy: ('a', 'b', 'c')
polite: None
other: None
-----------
> -g a
greedy: ('a',)
polite: None
other: None
-----------
> -g
Error: -g option requires an argument
-----------
> extra
Usage: test.py [OPTIONS]
Error: Got unexpected extra argument (extra)
-----------
> --help
Usage: test.py [OPTIONS]
Options:
-g TEXT
--polite TEXT
--other TEXT
--help Show this message and exit.
You can use this trick.
import click
#click.command()
#click.option('--users', nargs=0, required=True)
#click.argument('users', nargs=-1)
#click.option('--bar')
def fancy_command(users, bar):
users_str = ', '.join(users)
print('Users: {}. Bar: {}'.format(users_str, bar))
if __name__ == '__main__':
fancy_command()
Add fake option with a needed name and none arguments nargs=0, then add 'argument' with the unlimited args nargs=-1.
$ python foo --users alice bob charlie --bar baz
Users: alice, bob, charlie. Bar: baz
But be careful with the further options:
$ python foo --users alice bob charlie --bar baz faz
Users: alice, bob, charlie, faz. Bar: baz
I ran into the same issue. Instead of implementing a single command line option with n number of arguments, I decided to use multiple of the same command line option and just letting Click make a tuple out of the arguments under the hood. I ultimately figured if Click didn't support it, that decision was probably made for a good reason.
https://click.palletsprojects.com/en/7.x/options/#multiple-options
here is an example of what I am saying:
instead of passing a single string argument a splitting on a delimiter:
commit -m foo:bar:baz
I opted to use this:
commit -m foo -m bar -m baz
here is the source code:
#click.command()
#click.option('--message', '-m', multiple=True)
def commit(message):
click.echo('\n'.join(message))
This is more to type, but I do think it makes the CLI more user friendly and robust.
I needed this for myself and thought of settling for the solution provided by #nikita-malovichko even though it is very restrictive, but it didn't work for me (see my comment to that answer) so came up with the below alternative.
My solution doesn't directly address the question on how to support nargs=*, but it provided a good alternative for myself so sharing it for the benefit of others.
The idea is to use one option that specifies the expected count for another, i.e., set the nargs count dynamically at runtime. Here is a quick demo:
import click
def with_dynamic_narg(cnt_opt, tgt_opt):
class DynamicNArgSetter(click.Command):
def parse_args(self, ctx, args):
ctx.resilient_parsing = True
parser = self.make_parser(ctx)
opts, _, _ = parser.parse_args(args=list(args))
if cnt_opt in opts:
for p in self.params:
if isinstance(p, click.Option) and p.name == tgt_opt:
p.nargs = int(opts[cnt_opt])
ctx.resilient_parsing = False
return super().parse_args(ctx, args)
return DynamicNArgSetter
#click.command(cls=with_dynamic_narg('c', 'n'))
#click.option("-c", type=click.INT)
#click.option("-n", nargs=0)
def f(c, n):
print(c, n)
if __name__ == '__main__':
f()
In the above code, a custom Command class is created that knows the link between the "count" arg and the target arg that takes multiple args. It first does a local parsing in "resilient" mode to detect the count, then uses the count to update the nargs value of the target arg and then resumes parsing in normal mode.
Here is some sample interaction:
$ python t.py -c 0
0 None
$ python t.py -c 1
Usage: t.py [OPTIONS]
Try 't.py --help' for help.
Error: Missing option '-n'.
$ python t.py -c 0 -n a
Usage: t.py [OPTIONS]
Try 't.py --help' for help.
Error: Got unexpected extra argument (a)
$ python t.py -c 1 -n a
1 a
$ python /tmp/t.py -c 2 -n a b
2 ('a', 'b')
Note: The advantage over the official recommendation of using multiple=True is that we can use filename wildcards and let shell expand them. E.g.,
$ touch abc.1 abc.2
$ python t.py -c 2 -n abc.*
2 ('abc.1', 'abc.2')
$ python t.py -c $(echo abc.* | wc -w) -n abc.*
2 ('abc.1', 'abc.2')
This code:
#!/usr/bin env python3
import click
def f(*a, **kw):
print(a, kw)
commands = [click.Command("cmd1", callback=f), click.Command("cmd2", callback=f)]
cli = click.Group(commands={c.name: c for c in commands})
if __name__ == "__main__":
cli()
generates this help:
# Usage: cli.py [OPTIONS] COMMAND [ARGS]...
# Options:
# --help Show this message and exit.
# Commands:
# cmd1
# cmd2
I have a lot of subcommands, so I want to divide them into sections in the help like this:
# Usage: cli.py [OPTIONS] COMMAND [ARGS]...
# Options:
# --help Show this message and exit.
# Commands:
# cmd1
# cmd2
#
# Extra other commands:
# cmd3
# cmd4
How can I split the commands into sections in the help like that, without affecting the functionality?
If you define your own group class you can overide the help generation like:
Custom Class:
class SectionedHelpGroup(click.Group):
"""Sections commands into help groups"""
def __init__(self, *args, **kwargs):
self.grouped_commands = kwargs.pop('grouped_commands', {})
commands = {}
for group, command_list in self.grouped_commands.items():
for cmd in command_list:
cmd.help_group = group
commands[cmd.name] = cmd
super(SectionedHelpGroup, self).__init__(
*args, commands=commands, **kwargs)
def command(self, *args, **kwargs):
help_group = kwargs.pop('help_group')
decorator = super(SectionedHelpGroup, self).command(*args, **kwargs)
def new_decorator(f):
cmd = decorator(f)
cmd.help_group = help_group
self.grouped_commands.setdefault(help_group, []).append(cmd)
return cmd
return new_decorator
def format_commands(self, ctx, formatter):
for group, cmds in self.grouped_commands.items():
rows = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
if cmd is None or cmd.help_group != group:
continue
rows.append((subcommand, cmd.short_help or ''))
if rows:
with formatter.section(group):
formatter.write_dl(rows)
Using the Custom Class:
Pass the Custom Class to click.group() using cls parameter like:
#click.group(cls=SectionedHelpGroup)
def cli():
""""""
when defining commands, pass the help group the command belongs to like:
#cli.command(help_group='my help group')
def a_command(*args, **kwargs):
....
How does this work?
This works because click is a well designed OO framework. The #click.group() decorator usually instantiates a click.Group object but allows this behavior to be over-ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride the desired methods.
In this case, we hook the command() decorator to allow the help_group to be identified. We also override the format_commands() method to print the commands help into the groups.
Test Code:
import click
def f(*args, **kwargs):
click.echo(args, kwargs)
commands = {
'help group 1': [
click.Command("cmd1", callback=f),
click.Command("cmd2", callback=f)
],
'help group 2': [
click.Command("cmd3", callback=f),
click.Command("cmd4", callback=f)
]
}
cli = SectionedHelpGroup(grouped_commands=commands)
#cli.command(help_group='help group 3')
def a_command(*args, **kwargs):
"""My command"""
click.echo(args, kwargs)
if __name__ == "__main__":
cli(['--help'])
Results:
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
help group 1:
cmd1
cmd2
help group 2:
cmd3
cmd4
help group 3:
a_command My command
Stephen's answer above gives the general principle. If you only add new commands to a group using add_command, it can be simplified slightly:
import click
import collections
class SectionedHelpGroup(click.Group):
"""Organize commands as sections"""
def __init__(self, *args, **kwargs):
self.section_commands = collections.defaultdict(list)
super().__init__(*args, **kwargs)
def add_command(self, cmd, name=None, section=None):
self.section_commands[section].append(cmd)
super().add_command(cmd, name=name)
def format_commands(self, ctx, formatter):
for group, cmds in self.section_commands.items():
with formatter.section(group):
formatter.write_dl(
[(cmd.name, cmd.get_short_help_str() or "") for cmd in cmds]
)
Example
def f(*args, **kwargs):
click.echo(args, kwargs)
commands = {
'help group 1': [
click.Command("cmd1", callback=f),
click.Command("cmd2", callback=f)
],
'help group 2': [
click.Command("cmd3", callback=f),
click.Command("cmd4", callback=f)
]
}
#click.group(
help=f"Sectioned Commands CLI",
cls=SectionedHelpGroup
)
def cli():
pass
for (section, cmds) in commands.items():
for cmd in cmds:
cli.add_command(cmd, section=section)
if __name__ == "__main__":
cli()
Say my CLI utility has three commands: cmd1, cmd2, cmd3
And I want cmd3 to have same options and flags as cmd1 and cmd2. Like some sort of inheritance.
#click.command()
#click.options("--verbose")
def cmd1():
pass
#click.command()
#click.options("--directory")
def cmd2():
pass
#click.command()
#click.inherit(cmd1, cmd2) # HYPOTHETICAL
def cmd3():
pass
So cmd3 will have flag --verbose and option --directory. Is it possible to make this with Click? Maybe I just have overlooked something in the documentation...
EDIT: I know that I can do this with click.group(). But then all the group's options must be specified before group's command. I want to have all the options normally after command.
cli.py --verbose --directory /tmp cmd3 -> cli.py cmd3 --verbose --directory /tmp
I have found a simple solution! I slightly edited the snippet from https://github.com/pallets/click/issues/108 :
import click
_cmd1_options = [
click.option('--cmd1-opt')
]
_cmd2_options = [
click.option('--cmd2-opt')
]
def add_options(options):
def _add_options(func):
for option in reversed(options):
func = option(func)
return func
return _add_options
#click.group()
def group(**kwargs):
pass
#group.command()
#add_options(_cmd1_options)
def cmd1(**kwargs):
print(kwargs)
#group.command()
#add_options(_cmd2_options)
def cmd2(**kwargs):
print(kwargs)
#group.command()
#add_options(_cmd1_options)
#add_options(_cmd2_options)
#click.option("--cmd3-opt")
def cmd3(**kwargs):
print(kwargs)
if __name__ == '__main__':
group()
Define a class with common parameters
class StdCommand(click.core.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.params.insert(0, click.core.Option(('--default-option',), help='Every command should have one'))
Then pass the class to decorator when defining the command function
#click.command(cls=StdCommand)
#click.option('--other')
def main(default_option, other):
...
You could also have another decorator for shared options. I found this solution here
def common_params(func):
#click.option('--foo')
#click.option('--bar')
#functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
#click.command()
#common_params
#click.option('--baz')
def cli(foo, bar, baz):
print(foo, bar, baz)
This code extracts all the options from it's arguments
def extract_params(*args):
from click import Command
if len(args) == 0:
return ['']
if any([ not isinstance(a, Command) for a in args ]):
raise TypeError('Handles only Command instances')
params = [ p.opts() for cmd_inst in args for p in cmd_inst.params ]
return list(set(params))
now you can use it:
#click.command()
#click.option(extract_params(cmd1, cmd2))
def cmd3():
pass
This code extracts only the parameters and none of their default values, you can improve it if needed.
A slight improvement on #jirinovo solution.
this version support an unlimited number of click options.
one thing that is worth mentioning, the order you pass the options is important
import click
_global_options = [click.option('--foo', '-f')]
_local_options = [click.option('--bar', '-b', required=True)]
_local_options2 = [click.option('--foofoo', required=True)]
def add_options(*args):
def _add_options(func):
options = [x for n in args for x in n]
for option in reversed(options):
func = option(func)
return func
return _add_options
#click.group()
def cli():
pass
#cli.group()
def subcommand():
pass
#subcommand.command()
#add_options(_global_options, _local_options)
def echo(foo, bar):
print(foo, bar, sep='\n')
#subcommand.command()
#add_options(_global_options)
def echo2(foo):
print(foo)
#subcommand.command()
#add_options(_global_options, _local_options2)
def echo3(foo, foofoo):
print(foo, foofoo, sep='\n')
#subcommand.command()
#add_options(_global_options, _local_options, _local_options2)
def echo4(foo, bar, foofoo):
print(foo, bar, foofoo, sep='\n')
if __name__ == '__main__':
cli()
I'm writing a relatively simple Python script which supports a couple of different commands. The different commands support different options and I want to be able to pass the options parsed by argparse to the correct method for the specified command.
The usage string looks like so:
usage: script.py [-h]
{a, b, c}
...
script.py: error: too few arguments
I can easily call the appropriate method:
def a():
...
def b():
...
def c():
...
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.set_defaults(method = a)
...
arguments = parser.parse_args()
arguments.method()
However, I have to pass arguments to these methods and they all accept different sets of arguments.
Currently, I just pass the Namespace object returned by argparse, like so:
def a(arguments):
arg1 = getattr(arguments, 'arg1', None)
...
This seems a little awkward, and makes the methods a little harder to reuse as I have to pass arguments as a dict or namespace rather than as usual parameters.
I would like someway of defining the methods with parameters (as you would a normal function) and still be able to call them dynamically while passing appropriate parameters. Like so:
def a(arg1, arg2):
...
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.set_defaults(method = a)
...
arguments = parser.parse_args()
arguments.method() # <<<< Arguments passed here somehow
Any ideas?
I found quite a nice solution:
import argparse
def a(arg1, arg2, **kwargs):
print arg1
print arg2
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.set_defaults(method = a)
parser.add_argument('arg1', type = str)
parser.add_argument('arg2', type = str)
arguments = parser.parse_args()
arguments.method(**vars(arguments))
Of course there's a minor problem if the arguments of the method clash with the names of the arguments argparse uses, though I think this is preferable to passing the Namespace object around and using getattr.
You're probably trying to achieve the functionality that sub-commands provide:
http://docs.python.org/dev/library/argparse.html#sub-commands
Not sure how practical this is, but by using inspect you can leave out the extraneous **kwargs parameter on your functions, like so:
import argparse
import inspect
def sleep(seconds=0):
print "sleeping", seconds, "seconds"
def foo(a, b=2, **kwargs):
print "a=",a
print "b=",b
print "kwargs=",kwargs
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="subcommand")
parser_sleep = subparsers.add_parser('sleep')
parser_sleep.add_argument("seconds", type=int, default=0)
parser_sleep.set_defaults(func=sleep)
parser_foo = subparsers.add_parser('foo')
parser_foo.add_argument("-a", type=int, default=101)
parser_foo.add_argument("-b", type=int, default=201)
parser_foo.add_argument("--wacky", default=argparse.SUPPRESS)
parser_foo.set_defaults(func=foo)
args = parser.parse_args()
arg_spec = inspect.getargspec(args.func)
if arg_spec.keywords:
## convert args to a dictionary
args_for_func = vars(args)
else:
## get a subset of the dictionary containing just the arguments of func
args_for_func = {k:getattr(args, k) for k in arg_spec.args}
args.func(**args_for_func)
Examples:
$ python test.py sleep 23
sleeping 23 seconds
$ python test.py foo -a 333 -b 444
a= 333
b= 444
kwargs= {'func': <function foo at 0x10993dd70>}
$ python test.py foo -a 333 -b 444 --wacky "this is wacky"
a= 333
b= 444
kwargs= {'func': <function foo at 0x10a321d70>, 'wacky': 'this is wacky'}
Have fun!