How can I define the order of click sub-commands in "--help"
Asked Answered
D

3

11

I have code like this:

import click

@click.group()
def entry_point():
    pass

entry_point.add_command(lidtk.data.download_documents.main)
entry_point.add_command(lidtk.data.create_ml_dataset.main)
entry_point.add_command(lidtk.classifiers.text_cat.textcat_ngram.cli)

which gives the help text:

lidtk --help
Usage: lidtk [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  create-dataset  Create sharable dataset from downloaded...
  download        Download 1000 documents of each language.
  textcat

which is over all pretty close to what I want. But I would like to change the order to:

Commands:
  download        Download 1000 documents of each language.
  create-dataset  Create sharable dataset from downloaded...
  textcat

How can this be done using click?

Daphie answered 25/12, 2017 at 23:16 Comment(0)
S
18

The order of the commands listed by help is set by the list_commands() method of the click.Group class. So, one way to approach the desire to change the help listing order is to inherit for click.Group and override list_commands to give the desired order.

Custom Class

This class overrides the click.Group.command() method which is used to decorate command functions. It adds the ability to specify a help_priority, which allows the sort order to be modified as desired:

class SpecialHelpOrder(click.Group):

    def __init__(self, *args, **kwargs):
        self.help_priorities = {}
        super(SpecialHelpOrder, self).__init__(*args, **kwargs)

    def get_help(self, ctx):
        self.list_commands = self.list_commands_for_help
        return super(SpecialHelpOrder, self).get_help(ctx)

    def list_commands_for_help(self, ctx):
        """reorder the list of commands when listing the help"""
        commands = super(SpecialHelpOrder, self).list_commands(ctx)
        return (c[1] for c in sorted(
            (self.help_priorities.get(command, 1), command)
            for command in commands))

    def command(self, *args, **kwargs):
        """Behaves the same as `click.Group.command()` except capture
        a priority for listing command names in help.
        """
        help_priority = kwargs.pop('help_priority', 1)
        help_priorities = self.help_priorities

        def decorator(f):
            cmd = super(SpecialHelpOrder, self).command(*args, **kwargs)(f)
            help_priorities[cmd.name] = help_priority
            return cmd

        return decorator

Using the Custom Class

By passing the cls parameter to the click.group() decorator, any commands added to the group via the the group.command() can be passed a help_priority. The priorities default to 1, and lower numbers are printed first.

@click.group(cls=SpecialHelpOrder)
def cli():
    """My Excellent CLI"""

@cli.command(help_priority=5)
def my_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 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.

Steps here:

  1. Override Group.command() so that decorated commands can be passed a help_priority. In the over ridden decorator, capture the desired priority for later
  2. Override Group.get_help(). In the over ridden method, substitute Group.list_commands with a list_commands which will order the commands as desired.

Test Code:

import click

@click.group(cls=SpecialHelpOrder)
def cli():
    pass

@cli.command()
def command1():
    '''Command #1'''

@cli.command(help_priority=5)
def command2():
    '''Command #2'''

@cli.command()
def command3():
    '''Command #3'''

if __name__ == '__main__':
    cli('--help'.split())

Test Results:

Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  command1  Command #1
  command3  Command #3
  command2  Command #2
Sorbose answered 26/12, 2017 at 23:41 Comment(1)
I guess the real question is to list subcommands in the order they appear.Airla
M
14

It's a nice description at the top, but we can achieve it more easily

import click
import collections


from .aws import aws_group
from .db import db_group
from .front import front_group
from .celery import celery_group
from .i18n import i18n_group
from .deprecated import add_deprecated


class OrderedGroup(click.Group):
    def __init__(self, name=None, commands=None, **attrs):
        super(OrderedGroup, self).__init__(name, commands, **attrs)
        #: the registered subcommands by their exported names.
        self.commands = commands or collections.OrderedDict()

    def list_commands(self, ctx):
        return self.commands


@click.group(cls=OrderedGroup)
def entire_group():
    """Entire Group"""


entire_group.add_command(aws_group)
entire_group.add_command(db_group)
entire_group.add_command(front_group)
entire_group.add_command(celery_group)
entire_group.add_command(i18n_group)
add_deprecated(entire_group)

Just change self.commands from Dict to OrderedDict. As a result, my deprecated commands at a bottom of list.

Millner answered 10/10, 2019 at 13:14 Comment(0)
H
5

The answer by Максим Стукало helped me a lot. However, it was missing some typing info. Given, we always do strict typing and given I can't edit the post, I figured I'd create another post, maybe it helps someone:

import collections
from typing import Optional, Mapping

import click


class OrderedGroup(click.Group):
    def __init__(self, name: Optional[str] = None, commands: Optional[Mapping[str, click.Command]] = None, **kwargs):
        super(OrderedGroup, self).__init__(name, commands, **kwargs)
        #: the registered subcommands by their exported names.
        self.commands = commands or collections.OrderedDict()

    def list_commands(self, ctx: click.Context) -> Mapping[str, click.Command]:
        return self.commands

Hagen answered 31/12, 2021 at 12:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.