I am trying to figure out how to categorize commands in Click to resemble something close to the structure that kubectl uses in the way it separate commands out.
For instance, in a vanilla Click help output we have:
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
A CLI tool
Options:
-h, --help Show this message and exit.
Commands:
command1 This is command1
command2 This is command2
command3 This is command3
command4 This is command4
Instead, what would be ideal for my usage is to have a separation to better categorize the command structure.
For instance:
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
A CLI tool
Options:
-h, --help Show this message and exit.
Specific Commands for X:
command1 This is command1
command2 This is command2
Specific Commands for Y:
command3 This is command3
command4 This is command4
Global Commands:
version Shows version
I am using the latest Python and latest version of Click also for this.
I have tried looking into hooking into various Click classes to change this behaviour but have been unsuccessful in doing so.
Closest I have gotten is being able to structure commands based on priority but I am not able to logically separate them out as in the example above.
Any help would be greatly appreciated.
I've achieved this by creating my own click.Group:
class OrderedGroup(click.Group):
def __init__(self, name=None, commands=None, **attrs):
super(OrderedGroup, self).__init__(name, commands, **attrs)
self.commands = commands or collections.OrderedDict()
def list_commands(self, ctx):
return self.commands
def format_commands(self, ctx, formatter):
super().get_usage(ctx)
formatter.write_paragraph()
with formatter.section("Specific Commands for X:"):
formatter.write_text(
f'{self.commands.get("command1").name}\t\t{self.commands.get("command1").get_short_help_str()}')
formatter.write_text(
f"{self.commands.get('command2').name}\t\t{self.commands.get('command2').get_short_help_str()}")
with formatter.section("Specific Commands for Y:"):
formatter.write_text(
f'{self.commands.get("command3").name}\t\t{self.commands.get("command3").get_short_help_str()}')
formatter.write_text(
f'{self.commands.get("command4").name}\t\t{self.commands.get("command4").get_short_help_str()}')
with formatter.section("Global Commands"):
formatter.write_text(
f'{self.commands.get("version").name}\t\t{self.commands.get("version").get_short_help_str()}')
And created the cli group as such:
#click.group(cls=OrderedGroup)
def cli():
pass
Does this help?
Related
Why subprocess.run() freezes on this application?
import subprocess
subprocess.run('eumdac.exe')
The app is from official source: https://gitlab.eumetsat.int/eumetlab/data-services/eumdac/-/releases/1.2.0
Windows Binary: https://gitlab.eumetsat.int/eumetlab/data-services/eumdac/uploads/ddc0cac2c969efa51f000f4a5eccca59/eumdac-1.2.0-win.zip
This is what I am getting by running it in cmd.exe:
(project_directory)>eumdac
usage: eumdac [-h] [--version] [--set-credentials ConsumerKey ConsumerSecret] [--debug]
{describe,search,download,subscribe,tailor} ...
EUMETSAT Data Access Client
positional arguments:
{describe,search,download,subscribe,tailor}
describe describe a collection or product
search search for products at the collection level
download download product(s) from a collection
subscribe subscribe a server for a collection
tailor tailoring product(s) from collection
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--set-credentials ConsumerKey ConsumerSecret
permanently set consumer key and secret and exit, see https://api.eumetsat.int/api-key
--debug show backtrace for errors
PS. "cmd /c eumdac.exe" neither works.
You can use subprocess.run(['powershell', './eumdac.exe', '-h'])
To capture the output, you can use the following format.
output = subprocess.run(['powershell', './eumdac.exe', '-h'], capture_output=True)
print(output.stdout.decode('utf-8'))
I have the following click code:
#click.group(invoke_without_command=True)
def cli():
click.echo("Starting CallFlow....")
setup_logging()
# ##################----GEN---##################
#cli.command(help="a sub command")
#click.option(
"--folder", help="Tests folder path", type=str, nargs=1,
)
def sub1(folder):
# run some code here
Running my prog name that uses the above cli like this:
prog-name --help
shows me the correct help text:
Usage: prog-name [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
sub1 Help text
But running
prog-name sub1 --help --folder
I get an error that folder requires an argument like so:
Error: --folder option requires an argument
I thought that --help was an eager parameter and gets evaluated first. shouldn't that produce a help text?
From the documentation, the concept of eagerness refers only to the order of execution. Usually, the command line options will be processed in the order they are defined; making options like --help and --version eager means that they will be evaluated first.
If --help were not eager, your example would require --folder to always be passed first, like:
prog-name sub1 --folder test_folder --help
Context:
I'm having several scripts with loads of sub commands that I'd like to convert to using click
At the moment all these commands do accept -h and --help in order to display help options. I'd like to keep this behavior.
Problem:
click accepts by default --help to display the help text, but not -h
for a click command this can be changed easily by adding.
#click.group()
#click.help_option("--help", "-h")
def cli():
""" the doc string """
enter code here
#cli.command()
#click.help_option("--help", "-h")
def mycommand()
pass
#cli.command()
#click.help_option("--help", "-h")
def mycommand1()
pass
...
However if I'm having tens of commands I have to reapply the decorator line
#click.help_option("--help", "-h")
fort each sub command.
Would there be any trick to avoid having to write this line everywhere?
You need to define a CONTEXT_SETTINGS and use it like this:
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.command(context_settings=CONTEXT_SETTINGS)
def cli():
pass
From the click documentation:
Help Parameter Customization Changelog The help parameter is
implemented in Click in a very special manner. Unlike regular
parameters it’s automatically added by Click for any command and it
performs automatic conflict resolution. By default it’s called --help,
but this can be changed. If a command itself implements a parameter
with the same name, the default help parameter stops accepting it.
There is a context setting that can be used to override the names of
the help parameters called help_option_names.
This example changes the default parameters to -h and --help instead
of just --help:
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.command(context_settings=CONTEXT_SETTINGS) def cli():
pass And what it looks like:
$ cli -h Usage: cli [OPTIONS]
Options: -h, --help Show this message and exit.
I am trying to make my python script very user friendly, so I like to write some sort of help for it. What is your advise for this? I could just put in some logic that if the user passed help as a paramater to the script, they get help. Is there a best practise or convention for this?
Use argparse.
For example, with test.py:
import argparse
parser=argparse.ArgumentParser(
description='''My Description. And what a lovely description it is. ''',
epilog="""All is well that ends well.""")
parser.add_argument('--foo', type=int, default=42, help='FOO!')
parser.add_argument('bar', nargs='*', default=[1, 2, 3], help='BAR!')
args=parser.parse_args()
Running
% test.py -h
yields
usage: test.py [-h] [--foo FOO] [bar [bar ...]]
My Description. And what a lovely description it is.
positional arguments:
bar BAR!
optional arguments:
-h, --help show this help message and exit
--foo FOO FOO!
All is well that ends well.
Best practice is to use argparse to handle all your commandline arguments. It includes a default --help which you can customize to your likings.
Here's the simplest example:
import argparse
parser = argparse.ArgumentParser(description='This is my help')
args = parser.parse_args()
Which results in:
% python argparse_test.py -h
usage: argparse_test.py [-h]
This is my help
optional arguments:
-h, --help show this help message and exit
You can define all your arguments with argparse and set a help message for each one of them. The resulting filtered/validated arguments are returned by parser.parse_args().
An alternative to the built-in argparse is a 3rd-party package called Click which features "automatic help page generation" and "arbitrary nesting of commands" (which also produces nested help pages). Internally, it's based on argparse, but, for me, makes the creation of complex CLI more convenient using decorators.
Here's a sample code:
import click
#click.command()
#click.argument("things", nargs=-1)
#click.option("-t", show_default=True, default="int", help="Data type")
#click.option("-o", help="Output format")
def combine(things, t):
"""Combines things into a single element"""
pass
if __name__ == "__main__":
combine()
And the generated help page:
$ python myapp.py --help
Usage: myapp.py [OPTIONS] [THINGS]...
Combines things into a single element
Options:
-t TEXT Data type [default: int]
-o TEXT Output format
--help Show this message and exit.
One of the nice things about it is that it uses the method docstrings as part of the help page, which is convenient because the docstring can now be used both for developer documentation and for script usage help.
You can also have nested command groups:
import click
#click.command()
#click.argument("numbers", nargs=-1)
#click.option("-e", help="Extra option for add")
def add(numbers, e):
"""Adds numbers"""
print(f"This method should add {numbers}")
#click.command()
#click.argument("numbers", nargs=-1)
#click.option("-e", help="Extra option for mul")
def mul(numbers, e):
"""Multiplies numbers"""
print(f"This method should multiply {numbers}")
#click.group()
def calc():
pass
calc.add_command(add)
calc.add_command(mul)
if __name__ == "__main__":
calc()
And it will produce nested help pages:
$ python myapp.py --help
Usage: myapp.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
add Adds numbers
mul Multiplies numbers
$ python myapp.py add --help
Usage: myapp.py add [OPTIONS] [NUMBERS]...
Adds numbers
Options:
-e TEXT Extra option for add
--help Show this message and exit.
$ python myapp.py mul --help
Usage: myapp.py mul [OPTIONS] [NUMBERS]...
Multiplies numbers
Options:
-e TEXT Extra option for mul
--help Show this message and exit.
For more information, see the Documenting Scripts section of the docs.
I am using click within a local module and I would like to adjust how the help is displayed:
Currently output with --help:
Usage: __main__.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
foo Foo is a program very nice and pretty...
By default the prog name is __main__.py and the text is trimmed to 78 chars.
I discovered that this can be adjusted using the HelpFormatter class. But I don't know how to use it in this context.
Current Code:
import click
#click.group()
def main(ctx):
pass
#main.command()
def foo():
pass
click.CommandCollection(sources=[main])()
Expected output:
Usage: my_module_name [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
foo Foo is a program very nice and pretty and this sentence is very long.
If you are trying to to avoid the truncation of the help string, this can be accomplished via the short_help parameter. short_help is generally derived from help but truncated. If passed explicitly, the entire string will be displayed.
To display the string my_module_name, that can be passed under the parameter prog_name
Test Code:
import click
#click.group()
def main(ctx):
pass
#main.command(short_help='Foo is a program very nice and pretty and '
'this sentence is very long.')
def foo():
pass
main(['--help'], prog_name='my_module_name')
Results of short_help:
Usage: my_module_name [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
foo Foo is a program very nice and pretty and this sentence is very long.