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()
Related
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.
The Problem
I 'need' to count the number of arguments expected to be passed to a function object. My commands are stored via their object, so I can reference each one individually. I already tried inspect and while it seems perfect, it for some reason cannot distinguish between 0 arguments and 1 argument as both output 1. It seems I will have to link the actual code as it must be an error with my personal script. Here is the code:
# ============== Impots ==============
from shlex import split
from inspect import getfullargspec
# ============== Main ==============
class Command:
def __init__(self, fn, name=None, aliases=None):
self.name = name or fn.__name__
self.aliases = aliases or []
self.fn = fn
def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs)
#property
def all_names(self):
return (self.name, *self.aliases)
# ============= Storage =============
class Commands(dict):
def __init__(self, not_found: str='{} was not recognized.', argmismatch: str='{} takes {} arguments but {} were given.'):
self.flag = [not_found, argmismatch]
super().__init__()
def _add_command(self, command):
for cmd_name in command.all_names:
if cmd_name in self:
raise ValueError(f'Name or alias assigned to function {command.fn.__name__} is duplicate: {cmd_name}')
self[cmd_name] = command
def add_command(self, name=None, aliases=None):
def inner_fn(fn):
self._add_command(Command(fn, name, aliases))
return fn
return inner_fn
def execute(self, user_input):
self.command, *args = self.parse(user_input or 'no_input')
if self.command.lower() in self:
# THIS IS WHERE THE ISSUE HAPPENS
print(f'Passed: {len(args)}')
print(f'Expected: {len(getfullargspec(self[self.command])[0])}')
if not (len(args)==len(getfullargspec(self[self.command])[0])):
return (False, self.flag[1].format(self.command, len(getfullargspec(self[self.command])[0]), len(args)))
self[self.command](*args)
return (True, 'Command found!')
else:
return (False, self.flag[0].format(self.command))
#staticmethod
def parse(string):
if (string=='no_input'):
return ['','']
return split(string)
cmd = Commands()
#cmd.add_command(name='foo')
def foo():
print('bar')
cmd.execute('foo bar')
Not the Problem
I have already tried seeing if the list was filled with a Null value, it is not.
It does this across platforms (VSCode, Repl.it, PyDroid) so it is not a weird os issue.
I'm not sure if it could be len() treating it weirdly, but I know a normal empty list such as [] is counted as one.
I'm using this for the error message and possibly for auto-tips on what might have been wrong with the command, I would love any help and will update my post with new info and possible, correct, and incorrect solutions.
Similar Posts
This post is similar in question but the solution provided does not match what I require.
You don't need getfullargspec if you only access the zeroth element.
my_funct.__code__.co_argcount
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()
I am trying to use the Python library Click, but struggle to get an example working. I defined two groups, one of which (group2) is meant to handle common parameters for this group of commands. What I want to achieve is that those common parameters get processed by the group function (group2) and assigned to the context variable, so they can be used by the actual commands.
A use case would be a number of commands that require username and password, while some others don't (not even optionally).
This is the code
import click
#click.group()
#click.pass_context
def group1(ctx):
pass
#click.group()
#click.option('--optparam', default=None, type=str)
#click.option('--optparam2', default=None, type=str)
#click.pass_context
def group2(ctx, optparam):
print 'in group2', optparam
ctx['foo'] = create_foo_by_processing_params(optparam, optparam2)
#group2.command()
#click.pass_context
def command2a(ctx):
print 'command2a', ctx['foo']
#group2.command()
#click.option('--another-param', default=None, type=str)
#click.pass_context
def command2b(ctx, another_param):
print 'command2b', ctx['foo'], another_param
# many more more commands here...
# #group2.command()
# def command2x():
# ...
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
print 'In command2', argument1, option1
cli = click.CommandCollection(sources=[group1, group2])
if __name__ == '__main__':
cli(obj={})
And this is the result when using command2:
$ python cli-test.py command2 --optparam=123
> Error: no such option: --optparam`
What's wrong with this example. I tried to follow the docs closely, but opt-param doesn't seem to be recognised.
The basic issue with the desired scheme is that click.CommandCollection does not call the group function. It skips directly to the command. In addition it is desired to apply options to the group via decorator, but have the options parsed by the command. That is:
> my_prog my_command --group-option
instead of:
> my_prog --group-option my_command
How?
This click.Group derived class hooks the command invocation for the commands to intercept the group parameters, and pass them to the group command.
In Group.add_command, add the params to the command
In Group.add_command, override command.invoke
In overridden command.invoke, take the special args inserted from the group and put them into ctx.obj and remove them from params
In overridden command.invoke, invoke the group command, and then the command itself
Code:
import click
class GroupWithCommandOptions(click.Group):
""" Allow application of options to group with multi command """
def add_command(self, cmd, name=None):
click.Group.add_command(self, cmd, name=name)
# add the group parameters to the command
for param in self.params:
cmd.params.append(param)
# hook the commands invoke with our own
cmd.invoke = self.build_command_invoke(cmd.invoke)
self.invoke_without_command = True
def build_command_invoke(self, original_invoke):
def command_invoke(ctx):
""" insert invocation of group function """
# separate the group parameters
ctx.obj = dict(_params=dict())
for param in self.params:
name = param.name
ctx.obj['_params'][name] = ctx.params[name]
del ctx.params[name]
# call the group function with its parameters
params = ctx.params
ctx.params = ctx.obj['_params']
self.invoke(ctx)
ctx.params = params
# now call the original invoke (the command)
original_invoke(ctx)
return command_invoke
Test Code:
#click.group()
#click.pass_context
def group1(ctx):
pass
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
click.echo('In command2 %s %s' % (argument1, option1))
#click.group(cls=GroupWithCommandOptions)
#click.option('--optparam', default=None, type=str)
#click.option('--optparam2', default=None, type=str)
#click.pass_context
def group2(ctx, optparam, optparam2):
# create_foo_by_processing_params(optparam, optparam2)
ctx.obj['foo'] = 'from group2 %s %s' % (optparam, optparam2)
#group2.command()
#click.pass_context
def command2a(ctx):
click.echo('command2a foo:%s' % ctx.obj['foo'])
#group2.command()
#click.option('--another-param', default=None, type=str)
#click.pass_context
def command2b(ctx, another_param):
click.echo('command2b %s %s' % (ctx['foo'], another_param))
cli = click.CommandCollection(sources=[group1, group2])
if __name__ == '__main__':
cli('command2a --optparam OP'.split())
Results:
command2a foo:from group2 OP None
This isn't the answer I am looking for, but a step towards it. Essentially a new kind of group is introduced (GroupExt) and the option added to the group is now being added to the command.
$ python cli-test.py command2 --optparam=12
cli
command2 12
import click
class GroupExt(click.Group):
def add_command(self, cmd, name=None):
click.Group.add_command(self, cmd, name=name)
for param in self.params:
cmd.params.append(param)
#click.group()
def group1():
pass
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
print 'In command2', argument1, option1
# Equivalent to #click.group() with special group
#click.command(cls=GroupExt)
#click.option('--optparam', default=None, type=str)
def group2():
print 'in group2'
#group2.command()
def command2(optparam):
print 'command2', optparam
#click.command(cls=click.CommandCollection, sources=[group1, group2])
def cli():
print 'cli'
if __name__ == '__main__':
cli(obj={})
This is not quite what I am looking for. Ideally, the optparam would be handled by group2 and the results placed into the context, but currently it's processed in the command2. Perhaps someone knows how to extend this.
CliRunner lists no parameter to provide a context in its documentation.
The following should qualify as a minimum working example.
The real problem is a bit different.
It could be solved by moving the click decorated function into its own function for test coverage. Then the click function would be rendered almost useless.
import click
from click.testing import CliRunner
class Config():
def __init__(self):
self.value = 651
#click.command()
#click.pass_context
def print_numberinfo(ctx):
if not hasattr(ctx.obj, 'value'):
ctx.obj = Config()
click.echo(ctx.obj.value)
def test_print_numberinfo():
ctx = click.Context(print_numberinfo, obj = Config())
ctx.obj.value = 777
runner = CliRunner()
# how do I pass ctx to runner.invoke?
result = runner.invoke(print_numberinfo)
assert result.output == str(ctx.obj.value) + '\n'
You would directly pass your Config instance as keyword argument obj to runner.invoke:
import click
from click.testing import CliRunner
class Config():
def __init__(self):
self.value = 651
#click.command()
#click.pass_obj
def print_numberinfo(obj):
if not hasattr(obj, 'value'):
obj = Config()
click.echo(obj.value)
def test_print_numberinfo():
obj = Config()
obj.value = 777
runner = CliRunner()
# how do I pass ctx to runner.invoke?
result = runner.invoke(print_numberinfo, obj=obj)
assert result.output == str(obj.value) + '\n'
For someone who just want to make context.obj works like call from command-line:
CliRunner().invoke(commands.cli, ['sayhello'], catch_exceptions=False)
The first argument should be the the root group of click, then you can pass the command you want to call to the second argument(that is sayhello).
How commands.py like:
# !/usr/bin/env python
# coding: utf-8
import click
#click.group()
#click.pass_context
def cli(ctx):
ctx.obj = {
'foo': 'bar'
}
#cli.command()
#click.pass_context
def sayehello(ctx):
click.echo('hello!' + ctx.obj)
Appreciate to geowurster providing the solution.