I'm trying to add help to the command line application using click library. As mentioned in official documentation,
For commands, a short help snippet is generated. By default, it’s the
first sentence of the help message of the command, unless it’s too
long. This can also be overridden
With simple #click.command everything works as expected:
import click
#click.command()
def cli():
"""This is sample description of script."""
if __name__ == '__main__':
cli()
Running this would display description for the script from the method's doscstring:
Usage: example.py [OPTIONS]
This is sample description of script.
Options:
--help Show this message and exit.
But I need to use CommandCollection, as I'm creating a script consisting from multiple commands. Here is an example from official help:
import click
#click.group()
def cli1():
pass
#cli1.command()
def cmd1():
"""Command on cli1"""
#click.group()
def cli2():
pass
#cli2.command()
def cmd2():
"""Command on cli2"""
cli = click.CommandCollection(sources=[cli1, cli2])
if __name__ == '__main__':
cli()
And I don't know how to add description to whole command collection. What I've tried so far:
provide help with additional short_help parameter
set __doc__ argument for cli parameter, after creating CommandCollection
add docstring to cli1 method, decorated with #click.group
Any help is much appreciated.
Just use help parameter:
cli = click.CommandCollection(sources=[cli1, cli2], help="This would be your description, dude!")
Related
I have a python script using the "click" library to run various actions. In the below code, I attempted to use a pre-existing function in a new function. I tried to follow the click documentation to use the Context invoke and forward (which seems to work). But I get the error on execution of the make_connection function: "use_connection() takes 0 positional arguments but 1 was given". I am unsure what I'm doing wrong here. Please help.
from click.core import Context
import pysftp
import click
...
#click.group()
def cli():
pass
#cli.command('make_connection')
def make_connection():
return pysftp.Connection(server, username=username, password=sftp_key)
#cli.command('use_connection')
#click.pass_context
def use_connection():
Context.forward(make_connection)
connection = Context.invoke(make_connection)
if __name__ == '__main__':
cli()
You need to define cli() like this:
def cli(ctx):
...
A context obj is passed to cli as a first argument.
I'm a new bee for python currently working on the Click module. So here I have a doubt to providing input for the main cli function only. But I want to provide the input for my all the function one by one. is it possible to click? Thanks for advance.
#click.option('--create', default='sub', help='Create')
#click.command()
def create(create):
click.echo('create called')
os.system('curl http://127.0.0.1:5000/create')
#click.option('--conn', default='in', help='connect to server')
#click.command()
def conn(conn):
click.echo('conn called')
os.system('curl http://127.0.0.1:5000/')
and my setup.py
from setuptools import setup
setup(
name="hello",
version='0.1',
py_modules=['hello'],
install_requires=[
'Click',
],
entry_points='''
[console_scripts]
hello=hello:cli
''',
)
My output expectation
$ hello --conn in
success
hello --create sub
success
Sounds to me like you want different commands based on input provided to your hello cli. For that reason, Click has the useful notion of a group, a collection of commands that can be invoked.
You can reorganize your code as follows:
#click.group()
def cli():
pass
#cli.command()
def create():
click.echo('create called')
os.system('curl http://127.0.0.1:5000/create')
#cli.command()
def conn():
click.echo('conn called')
os.system('curl http://127.0.0.1:5000/')
def main():
value = click.prompt('Select a command to run', type=click.Choice(list(cli.commands.keys()) + ['exit']))
while value != 'exit':
cli.commands[value]()
if __name__ == "__main__":
main()
and the calls would be:
$ hello con
$ hello create
It doesn't seem like you need the options, as you don't change the behaviour of each command based on the option being passed in or not. For more information, please refer to the commands and groups Click documentation
I'm starting a CLI pipe-type application project which will eventually have a rather large collection of commands (which will be further extensible with plug-in). As a result, I would like to categorise them in the --help text:
Here is how it looks now:
Usage: my_pipe [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
another_filter help about that filter
another_generator help about that generator
another_sink help about that sink
some_filter help about this filter
some_generator help about this generator
some_sink help about this sink
This is more or less how I would like it to look:
Usage: my_pipe [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
Generators:
some_generator help about this generator
another_generator help about that generator
Filters:
some_filter help about this filter
another_filter help about that filter
Sinks:
some_sink help about this sink
another_sink help about that sink
How can this be achieved? Note that apart from the look of --help, I'm happy with the flat logical command organisation. Also, sub-groups are not an option as they are not allowed inside a chain=True group.
If you inherit from click.Group you can add a bit of code to group the commands and then show those groups in the help.
Custom Class
class GroupedGroup(click.Group):
def command(self, *args, **kwargs):
"""Gather the command help groups"""
help_group = kwargs.pop('group', None)
decorator = super(GroupedGroup, self).command(*args, **kwargs)
def wrapper(f):
cmd = decorator(f)
cmd.help_group = help_group
return cmd
return wrapper
def format_commands(self, ctx, formatter):
# Modified fom the base class method
commands = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
if not (cmd is None or cmd.hidden):
commands.append((subcommand, cmd))
if commands:
longest = max(len(cmd[0]) for cmd in commands)
# allow for 3 times the default spacing
limit = formatter.width - 6 - longest
groups = {}
for subcommand, cmd in commands:
help_str = cmd.get_short_help_str(limit)
subcommand += ' ' * (longest - len(subcommand))
groups.setdefault(
cmd.help_group, []).append((subcommand, help_str))
with formatter.section('Commands'):
for group_name, rows in groups.items():
with formatter.section(group_name):
formatter.write_dl(rows)
Using the Custom Class
To make use of the custom class, use the cls parameter to pass the class to the click.group() decorator.
#click.group(cls=GroupedGroup)
def cli():
"""My awesome cli"""
Then for each command mark the help group for the command to be included in like:
#cli.command(group='A Help Group')
def command():
"""This is a command"""
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 overridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and override desired methods.
In this case we override the click.Group.command() decorator to gather up the desired help group for each command. Then we override the click.Group.format_commands() method to use those groups when constructing the help.
Test Code
import click
#click.group(cls=GroupedGroup)
def cli():
"""My awesome cli"""
#cli.command(group='Generators')
def some_generator():
"""This is Some Generator"""
#cli.command(group='Generators')
def another_generator():
"""This is Another Generator"""
#cli.command(group='Filters')
def some_filter():
"""This is Some Filter"""
#cli.command(group='Filters')
def another_filter():
"""This is Another Filter"""
cli()
Results
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My awesome cli
Options:
--help Show this message and exit.
Commands:
Filters:
another-filter This is Another Filter
some-filter This is Some Filter
Generators:
another-generator This is Another Generator
some-generator This is Some Generator
My Click 7.0 application has one group, having multiple commands, called by the main cli function like so:
import click
#click.group()
#click.pass_context
def cli(ctx):
"This is cli helptext"
click.echo('cli called')
click.echo('cli args: {0}'.format(ctx.args))
#cli.group(chain=True)
#click.option('-r', '--repeat', default=1, type=click.INT, help='repeat helptext')
#click.pass_context
def chainedgroup(ctx, repeat):
"This is chainedgroup helptext"
for _ in range(repeat):
click.echo('chainedgroup called')
click.echo('chainedgroup args: {0}'.format(ctx.args))
#chainedgroup.command()
#click.pass_context
def command1(ctx):
"This is command1 helptext"
print('command1 called')
print('command1 args: {0}'.format(ctx.args))
#chainedgroup.command()
#click.pass_context
def command2(ctx):
"This is command2 helptext"
print('command2 called')
print('command2 args: {0}'.format(ctx.args))
Run:
$ testcli --help
$ testcli chainedgroup --help
$ testcli chainedgroup command1 --help
The help-text displays as expected--except that the parent functions are inadvertently run in the process. A single conditional checking to see if '--help' is contained in ctx.args should be enough to solve this problem, but does anyone know how/when '--help' is passed? Because with this code, ctx.args is empty every time.
If argparse is not an option, how about:
if '--help' in sys.argv:
...
click stores the arguments passed to a command in a list. The method get_os_args() returns such list. You can check if --help is in that list to determine if the help flag was invoked. Something like the following:
if '--help' in click.get_os_args():
pass
It is prebuilt - Click looks like a decorator for argparse (Hurrah for common sense).
import click
#click.command()
#click.option('--count', default=1, help='Number of greetings.')
#click.option('--name', prompt='Your name',
help='The person to greet.')
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
click.echo('Hello %s!' % name)
if __name__ == '__main__':
hello()
So you can write
python cl.py --name bob
And see
Hello bob!
Help is already done (as it is argparse)
python cl.py --help
Usage: cl.py [OPTIONS]
Simple program that greets NAME for a total of COUNT times.
Options:
--count INTEGER Number of greetings.
--name TEXT The person to greet.
--help Show this message and exit.
Been busy only just had time to read into this.
Sorry for the delay
Why not use argparse ? It has excellent for CLI parsing.
This question is about the Python Click library.
I want click to gather my commandline arguments. When gathered, I want to reuse these values. I dont want any crazy chaining of callbacks, just use the return value. By default, click disables using the return value and calls sys.exit().
I was wondering how to correctly invoke standalone_mode (http://click.pocoo.org/5/exceptions/#what-if-i-don-t-want-that) in case I want to use the decorator style. The above linked doc only shows the usage when (manually) creating Commands using click.
Is it even possible? A minimal example is shown below. It illustrates how click calls sys.exit() directly after returning from gatherarguments
import click
#click.command()
#click.option('--name', help='Enter Name')
#click.pass_context
def gatherarguments(ctx, name):
return ctx
def usectx(ctx):
print("Name is %s" % ctx.params.name)
if __name__ == '__main__':
ctx = gatherarguments()
print(ctx) # is never called
usectx(ctx) # is never called
$ python test.py --name Your_Name
I would love this to be stateless, meaning, without any click.group functionality - I just want the results, without my application exiting.
Just sending standalone_mode as a keyword argument worked for me:
from __future__ import print_function
import click
#click.command()
#click.option('--name', help='Enter Name')
#click.pass_context
def gatherarguments(ctx, name):
return ctx
def usectx(ctx):
print("Name is %s" % ctx.params['name'])
if __name__ == '__main__':
ctx = gatherarguments(standalone_mode=False)
print(ctx)
usectx(ctx)
Output:
./clickme.py --name something
<click.core.Context object at 0x7fb671a51690>
Name is something