-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Equivalent of argparse's add_argument_group, help sections #373
Comments
Unfortunately, no, not at the moment. I think it's sensible to add this to Click itself, but I think Click is flexible enough such that this could also be implemented as a third-party extension. |
It seems that one could subclass |
I thought something like this: @click.command()
@click.option_group('Output options:', ['verbose', 'output'], indent=2)
@click.option('--verbose')
@click.option('--output')
def cli(verbose, output):
pass Thanks for the pointers, I'll look into it. This is a very common use case for any CLI that has more than just a few options. See httpie |
Or maybe that would be more efficient for nesting groups. And it looks nice as we can see sections just by reading the code. @click.command()
@click.option_group('Output options:', [
click.option('--verbose'),
click.option('--output')
])
def cli(verbose, output):
pass |
I like the second variant better, but we have to think about what happens if the user does this (or forbid it):
IMO we should simply forbid this. |
It would create another section with indentation and so recursively. I don't see the issue here except that it would hurt readability, but that's on the developer. For starters forbid it and add support for this later on if someone complains and proves it is useful. |
This should also cover arguments so I suggest the name @click.command()
@click.section('Request:', [
click.option('--json'),
click.argument('method'),
click.argument('url')
])
@click.section('Output options:', [
click.option('--quiet'),
click.option('--verbose'),
click.option('--output')
])
def cli(verbose, output):
pass Would require #375 |
What’s the current state of this issue? Is there already some code to share? |
This is rather verbose and hacky, but it seems to work. #!/usr/bin env python3
import click
class SectionedFormatter(click.formatting.HelpFormatter):
def __init__(self, *args, sections, **kwargs):
self.sections = sections
super().__init__(*args, **kwargs)
def write_dl(self, rows, *args, **kwargs):
cmd_to_section = {}
for section, commands in self.sections.items():
for command in commands:
cmd_to_section[command] = section
sections = {}
for subcommand, help in rows:
sections.setdefault(cmd_to_section.get(subcommand, "Commands"), []).append(
(subcommand, help)
)
for section_name, rows in sections.items():
if rows[0][0][0] == "-":
super().write_dl(rows)
else:
with super().section(section_name):
super().write_dl(rows)
class SectionedContext(click.Context):
def __init__(self, *args, sections, **kwargs):
self.sections = sections
super().__init__(*args, **kwargs)
def make_formatter(self):
"""Creates the formatter for the help and usage output."""
return SectionedFormatter(
sections=self.sections,
width=self.terminal_width,
max_width=self.max_content_width,
)
class SectionedGroup(click.Group):
def __init__(self, *args, sections, **kwargs):
self.sections = sections
super().__init__(self, *args, **kwargs)
def make_context(self, info_name, args, parent=None, **extra):
"""This function when given an info name and arguments will kick
off the parsing and create a new :class:`Context`. It does not
invoke the actual command callback though.
:param info_name: the info name for this invokation. Generally this
is the most descriptive name for the script or
command. For the toplevel script it's usually
the name of the script, for commands below it it's
the name of the script.
:param args: the arguments to parse as list of strings.
:param parent: the parent context if available.
:param extra: extra keyword arguments forwarded to the context
constructor.
"""
for key, value in click._compat.iteritems(self.context_settings):
if key not in extra:
extra[key] = value
ctx = SectionedContext(
self, info_name=info_name, parent=parent, sections=self.sections, **extra
)
with ctx.scope(cleanup=False):
self.parse_args(ctx, args)
return ctx
def format_commands(self, ctx, formatter):
"""Extra format methods for multi methods that adds all the commands
after the options.
"""
commands = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
# What is this, the tool lied about a command. Ignore it
if cmd is None:
continue
if cmd.hidden:
continue
commands.append((subcommand, cmd))
# allow for 3 times the default spacing
if len(commands):
limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
rows = []
for subcommand, cmd in commands:
help = cmd.get_short_help_str(limit)
rows.append((subcommand, help))
if rows:
formatter.write_dl(rows)
# User code:
def f(*args, **kwargs):
print(args, kwargs)
SECTIONS = {"Primary": ["cmd1", "cmd2"], "Extras": ["cmd3", "cmd4"]}
commands = [
click.Command("cmd1", callback=f, help="first"),
click.Command("cmd2", callback=f, help="second"),
click.Command("cmd3", callback=f, help="third"),
click.Command("cmd4", callback=f, help="fourth"),
]
cli = SectionedGroup(commands={c.name: c for c in commands}, sections=SECTIONS)
if __name__ == "__main__":
cli() Generates help:
|
I have a simple solution for this, that could easily be merged into the core code: from collections import OrderedDict
import click
class CustomOption(click.Option):
def __init__(self, *args, **kwargs):
self.help_group = kwargs.pop('help_group', None)
super(CustomOption, self).__init__(*args, **kwargs)
class CustomCommand(click.Command):
def format_options(self, ctx, formatter):
"""Writes all the options into the formatter if they exist."""
opts = OrderedDict()
for param in self.get_params(ctx):
rv = param.get_help_record(ctx)
if rv is not None:
if hasattr(param, 'help_group') and param.help_group:
opts.setdefault(str(param.help_group), []).append(rv)
else:
opts.setdefault('Options', []).append(rv)
for name, opts_group in opts.items():
with formatter.section(name):
formatter.write_dl(opts_group) Which you can use like so: @click.group()
def cli():
pass
@cli.command(cls=CustomCommand)
@click.option('--option1', cls=CustomOption, help_group="Group1")
@click.option('--option2', cls=CustomOption, help_group="Group2")
@click.option('--option3', cls=CustomOption, help_group="Group1")
def mycmnd(option1):
pass and will give you: $ cli mycmnd --help
Usage: test.py mycmnd [OPTIONS]
Group1:
--option1 TEXT
--option3 TEXT
Group2:
--option2 TEXT
Options:
--help Show this message and exit. |
I'm not sure about putting into click core itself, since in general this is pretty easy to implement with sub-classing (as you demonstrated). Perhaps it could be added to the docs here https://click.palletsprojects.com/en/7.x/documentation/ I've also thought about maintaining an external list of these sorts of sub classing examples. |
I'll leave that up to you :) I'm happy sub-classing, but obviously bringing in to core protects against any API changes in future versions
Always good to have more "official" best practice examples of utilizing click |
My two cents... From an API point of view, the solution proposed by @Diaoul is the best option for me. But I would rather avoid to write ":" at the end of section names. The formatting should be "standard" and consistent across applications, so ":" should be automatically included. @click.command()
@click.argument('path')
@click.section('Section A', [
click.option('--a1'),
click.option('--a2')
])
@click.section('Section B', [
click.option('--b1'),
click.option('--b2')
])
def cli(path, a1, a2, b1, b2):
pass About (mandatory) arguments, I would rather forbid to include them in these "option sections". Even if click allowed to attach a help string to them, they should be listed in their own (unnamed) section at the beginning of the command help message. You don't want to dig around sections to see what you have to provide to a command. Finally, I agree to forbid nested option groups: that would complicate implementation and likely worsen help readability. |
@thedrow Do you already know of https://github.com/click-contrib/click-option-group? |
@thedrow I've just packaged and uploaded a modified and extended version of @chrisjsewell code which I've used in a couple of projects (see https://github.com/janLuke/cloup). It just handles help formatting (no option group constraints like |
I'm the author of The following API that you proposed looks nicely: @click.command()
@click.argument('path')
@click.section('Section A', [
click.option('--a1'),
click.option('--a2')
])
@click.section('Section B', [
click.option('--b1'),
click.option('--b2')
])
def cli(path, a1, a2, b1, b2):
pass This more clean and unambiguous API solves the issues and cavities with decorators ordering and looks more convenient. |
@espdev Hey :) As I wrote, I didn't have much time for this. I gave a very (very) quick look to I have very little time and motivation to open a PR at the moment but of course you can feel free to take any idea from |
Yes, I once read this discussion, but switched to something else and forgot about it. I already added the link to README. :) Thanks for detailed clarification of your viewpoint. I completely understand you. I will try to take ideas from |
@jtrakk Could you give an example of how to add arguments and options to the commands? |
@pratzz I'd recommend using the better packaged versions instead of my original hack. :-) |
Other solutions seem to be for grouping options or arguments, I wish to group commands which is why your solution works for me. |
@pratzz In cloup you can group both subcommands and options. See https://github.com/janLuke/cloup/blob/master/README.rst#subcommand-sections |
Cool, thanks :) |
UPDATE: the PR was merged after several changes and released with Cloup v0.5.0. For more info, see the documentation (do a full refresh of the pages with Ctrl+F5 if you see the old docs). Despite I was reluctant months ago, I recently worked on adding "parameter group constraints" to cloup. I've opened a PR on my own repo because I'd like to have some feedback: janluke/cloup#4. There you can find an overview of the API but honestly you can get the gist of it by looking at this example. I'll leave the PR open for some days. The approach is quite different from click-option-group. Any feedback, especially from @espdev or anyone involved in the development of click-option-group or click, is highly appreciated. (Sorry, this is probably off-topic but this seems to be the only issue about option groups still open.) |
FWIW, I think ewels/rich-click implements something similar as well. |
Is this possible to have some kind of help sections? I have a rather long list of options and arguments, I'd like to classify them somehow in the rendered help.
argparse has a add_argument_group which renders like this
Is it possible to achieve the same behavior?
The text was updated successfully, but these errors were encountered: