Click: add help to click argument

Created on 19 May 2016  路  7Comments  路  Source: pallets/click

I would like to be able to specify help on positional arguments.

@click.group()
def cli():
    click.echo(u'shellfoundry - CloudShell shell command-line tool')
    pass

@cli.command()
@click.argument(u'name', help='Shell name to be created')
@click.argument(u'template', default=u'default', help='Template to be used')
def new(name, template):
    """
    Create a new shell based on a template.\r\n
    """
    NewCommandExecutor().new(name, template)

The above code throws the below exception:

Traceback (most recent call last):
File "C:\Python27lib\runpy.py", line 162, in _run_module_as_main
"__main__", fname, loader, pkg_name)
File "C:\Python27lib\runpy.py", line 72, in _run_code
exec code in run_globals
File "c:\work\GitHub\shellfoundry\shellfoundry__main__.py", line 6, in
from bootstrap import cli
File "shellfoundry\bootstrap.py", line 36, in
@click.argument(u'template', default=u'default', help='Template to be used')
File "C:\Python27lib\site-packages\click\decorators.py", line 151, in decorator
_param_memo(f, ArgumentClass(param_decls, attrs))
File "C:\Python27lib\site-packages\click\core.py", line 1693, in __init__
Parameter.
init(self, param_decls, required=required, *attrs)
TypeError: *
init
() got an unexpected keyword argument 'help'

Most helpful comment

The following code implements this feature request (with a few other related customizations), and acts as a drop-in replacement module for import click. (Click seems to me to actually be very flexible, and I would guess that using the following strategy would allow most of the internals to be overridden/extended if you wanted to.)

Example Usage:

from your_package import click

@click.command()
@click.argument('name', help='The name to print.')
@click.option('--count', default=1, help='The number of greetings.')
def hello(name, count):
    """
    This script prints "Hello <name>!" COUNT times.
    """
    for x in range(count):
        click.echo('Hello %s!' % name)

What the output looks like:

$ hello --help
Usage: hello <name> [OPTIONS]

  This script prints "Hello <name>!" COUNT times.

Arguments:
  name: <name>  The name to print.  [required]

[OPTIONS]:
  --count INTEGER  The number of greetings.
  -h, --help       Show this message and exit.

And of course, the code itself:

(Upstream docstrings omitted for brevity.)

# your_package/click.py

import click
import inspect as _inspect

from click import *
from click.formatting import join_options as _join_options

DEFAULT_CONTEXT_SETTINGS = dict(help_option_names=('-h', '--help'))

def _update_ctx_settings(context_settings):
    rv = DEFAULT_CONTEXT_SETTINGS.copy()
    if not context_settings:
        return rv
    rv.update(context_settings)
    return rv


class Command(click.Command):
    """
    :param options_metavar: The options metavar to display in the usage.
                            Defaults to ``[OPTIONS]``.
    :param args_before_options: Whether or not to display the options
                                        metavar before the arguments.
                                        Defaults to False.
    """
    def __init__(self, name, context_settings=None, callback=None, params=None,
                 help=None, epilog=None, short_help=None, add_help_option=True,
                 options_metavar='[OPTIONS]', args_before_options=True):
        super().__init__(
            name, callback=callback, params=params, help=help, epilog=epilog,
            short_help=short_help, add_help_option=add_help_option,
            context_settings=_update_ctx_settings(context_settings),
            options_metavar=options_metavar)
        self.args_before_options = args_before_options

    # overridden to support displaying args before the options metavar
    def collect_usage_pieces(self, ctx):
        rv = [] if self.args_before_options else [self.options_metavar]
        for param in self.get_params(ctx):
            rv.extend(param.get_usage_pieces(ctx))
        if self.args_before_options:
            rv.append(self.options_metavar)
        return rv

    # overridden to group arguments separately from options
    def format_options(self, ctx, formatter):
        args = []
        opts = []
        for param in self.get_params(ctx):
            rv = param.get_help_record(ctx)
            if rv is not None:
                if isinstance(param, click.Argument):
                    args.append(rv)
                else:
                    opts.append(rv)

        def print_args():
            if args:
                with formatter.section('Arguments'):
                    formatter.write_dl(args)

        def print_opts():
            if opts:
                with formatter.section(self.options_metavar):
                    formatter.write_dl(opts)

        if self.args_before_options:
            print_args()
            print_opts()
        else:
            print_opts()
            print_args()


# overridden to make sure our custom classes propagate recursively in trees of commands
class Group(click.Group):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, context_settings=_update_ctx_settings(
            kwargs.pop('context_settings', None)), **kwargs)

    def command(self, *args, **kwargs):
        return super().command(
            *args, cls=kwargs.pop('cls', Command) or Command, **kwargs)

    def group(self, *args, **kwargs):
        return super().group(
            *args, cls=kwargs.pop('cls', Group) or Group, **kwargs)


class Argument(click.Argument):
    """
    :param help: the help string.
    :param hidden: hide this option from help outputs.
                   Default is True, unless ``help`` is given.
    """
    def __init__(self, param_decls, required=None, help=None, hidden=None, **attrs):
        super().__init__(param_decls, required=required, **attrs)
        self.help = help
        self.hidden = hidden if hidden is not None else not help

    # overridden to customize the automatic formatting of metavars
    # for example, given self.name = 'query':
    # upstream | (optional) | this-method | (optional)
    # default behavior:
    # QUERY    | [QUERY]    | <query>     | [<query>]
    # when nargs > 1:
    # QUERY... | [QUERY...] | <query>, ... | [<query>, ...]
    def make_metavar(self):
        if self.metavar is not None:
            return self.metavar
        var = '' if self.required else '['
        var += '<' + self.name + '>'
        if self.nargs != 1:
            var += ', ...'
        if not self.required:
            var += ']'
        return var

    # this code is 90% copied from click.Option.get_help_record
    def get_help_record(self, ctx):
        if self.hidden:
            return

        any_prefix_is_slash = []

        def _write_opts(opts):
            rv, any_slashes = _join_options(opts)
            if any_slashes:
                any_prefix_is_slash[:] = [True]
            rv += ': ' + self.make_metavar()
            return rv

        rv = [_write_opts(self.opts)]
        if self.secondary_opts:
            rv.append(_write_opts(self.secondary_opts))

        help = self.help or ''
        extra = []

        if self.default is not None:
            if isinstance(self.default, (list, tuple)):
                default_string = ', '.join('%s' % d for d in self.default)
            elif _inspect.isfunction(self.default):
                default_string = "(dynamic)"
            else:
                default_string = self.default
            extra.append('default: {}'.format(default_string))

        if self.required:
            extra.append('required')
        if extra:
            help = '%s[%s]' % (help and help + '  ' or '', '; '.join(extra))

        return ((any_prefix_is_slash and '; ' or ' / ').join(rv), help)


def command(name=None, cls=None, **attrs):
    return click.command(name=name, cls=cls or Command, **attrs)


def group(name=None, cls=None, **attrs):
    return click.group(name=name, cls=cls or Group, **attrs)


def argument(*param_decls, cls=None, **attrs):
    return click.argument(*param_decls, cls=cls or Argument, **attrs)

EDIT: Here's an example of what it looks like integrated with Flask.

I'd be happy to open a PR (leaving the current Click behaviour as default; different from the sample code above) if upstream is interested in supporting this feature officially.

All 7 comments

I just encountered this.

http://click.pocoo.org/6/documentation/

Arguments cannot be documented this way. This is to follow the general convention of Unix tools of using arguments for only the most necessary things and to document them in the introduction text by referring to them by name.

Is this to say the best practice would be to document the argument in the docstring itself?

I think they mean to put a reference to the argument in the command long-help. For example, heres a script with command show that accepts an argument PATH.

[evan@blackbox ~] ph show -h
Usage: ph show [OPTIONS] PATH

show the contents of an entry, where PATH is the entry path

optional arguments:
  -h, --help     show this help message and exit
  --field FIELD  show the contents of a specific field as plaintext

So in this case you'd want to use both help and short_help, where short_help is a shorter command description that doesn't contain information about PATH.

The following code implements this feature request (with a few other related customizations), and acts as a drop-in replacement module for import click. (Click seems to me to actually be very flexible, and I would guess that using the following strategy would allow most of the internals to be overridden/extended if you wanted to.)

Example Usage:

from your_package import click

@click.command()
@click.argument('name', help='The name to print.')
@click.option('--count', default=1, help='The number of greetings.')
def hello(name, count):
    """
    This script prints "Hello <name>!" COUNT times.
    """
    for x in range(count):
        click.echo('Hello %s!' % name)

What the output looks like:

$ hello --help
Usage: hello <name> [OPTIONS]

  This script prints "Hello <name>!" COUNT times.

Arguments:
  name: <name>  The name to print.  [required]

[OPTIONS]:
  --count INTEGER  The number of greetings.
  -h, --help       Show this message and exit.

And of course, the code itself:

(Upstream docstrings omitted for brevity.)

# your_package/click.py

import click
import inspect as _inspect

from click import *
from click.formatting import join_options as _join_options

DEFAULT_CONTEXT_SETTINGS = dict(help_option_names=('-h', '--help'))

def _update_ctx_settings(context_settings):
    rv = DEFAULT_CONTEXT_SETTINGS.copy()
    if not context_settings:
        return rv
    rv.update(context_settings)
    return rv


class Command(click.Command):
    """
    :param options_metavar: The options metavar to display in the usage.
                            Defaults to ``[OPTIONS]``.
    :param args_before_options: Whether or not to display the options
                                        metavar before the arguments.
                                        Defaults to False.
    """
    def __init__(self, name, context_settings=None, callback=None, params=None,
                 help=None, epilog=None, short_help=None, add_help_option=True,
                 options_metavar='[OPTIONS]', args_before_options=True):
        super().__init__(
            name, callback=callback, params=params, help=help, epilog=epilog,
            short_help=short_help, add_help_option=add_help_option,
            context_settings=_update_ctx_settings(context_settings),
            options_metavar=options_metavar)
        self.args_before_options = args_before_options

    # overridden to support displaying args before the options metavar
    def collect_usage_pieces(self, ctx):
        rv = [] if self.args_before_options else [self.options_metavar]
        for param in self.get_params(ctx):
            rv.extend(param.get_usage_pieces(ctx))
        if self.args_before_options:
            rv.append(self.options_metavar)
        return rv

    # overridden to group arguments separately from options
    def format_options(self, ctx, formatter):
        args = []
        opts = []
        for param in self.get_params(ctx):
            rv = param.get_help_record(ctx)
            if rv is not None:
                if isinstance(param, click.Argument):
                    args.append(rv)
                else:
                    opts.append(rv)

        def print_args():
            if args:
                with formatter.section('Arguments'):
                    formatter.write_dl(args)

        def print_opts():
            if opts:
                with formatter.section(self.options_metavar):
                    formatter.write_dl(opts)

        if self.args_before_options:
            print_args()
            print_opts()
        else:
            print_opts()
            print_args()


# overridden to make sure our custom classes propagate recursively in trees of commands
class Group(click.Group):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, context_settings=_update_ctx_settings(
            kwargs.pop('context_settings', None)), **kwargs)

    def command(self, *args, **kwargs):
        return super().command(
            *args, cls=kwargs.pop('cls', Command) or Command, **kwargs)

    def group(self, *args, **kwargs):
        return super().group(
            *args, cls=kwargs.pop('cls', Group) or Group, **kwargs)


class Argument(click.Argument):
    """
    :param help: the help string.
    :param hidden: hide this option from help outputs.
                   Default is True, unless ``help`` is given.
    """
    def __init__(self, param_decls, required=None, help=None, hidden=None, **attrs):
        super().__init__(param_decls, required=required, **attrs)
        self.help = help
        self.hidden = hidden if hidden is not None else not help

    # overridden to customize the automatic formatting of metavars
    # for example, given self.name = 'query':
    # upstream | (optional) | this-method | (optional)
    # default behavior:
    # QUERY    | [QUERY]    | <query>     | [<query>]
    # when nargs > 1:
    # QUERY... | [QUERY...] | <query>, ... | [<query>, ...]
    def make_metavar(self):
        if self.metavar is not None:
            return self.metavar
        var = '' if self.required else '['
        var += '<' + self.name + '>'
        if self.nargs != 1:
            var += ', ...'
        if not self.required:
            var += ']'
        return var

    # this code is 90% copied from click.Option.get_help_record
    def get_help_record(self, ctx):
        if self.hidden:
            return

        any_prefix_is_slash = []

        def _write_opts(opts):
            rv, any_slashes = _join_options(opts)
            if any_slashes:
                any_prefix_is_slash[:] = [True]
            rv += ': ' + self.make_metavar()
            return rv

        rv = [_write_opts(self.opts)]
        if self.secondary_opts:
            rv.append(_write_opts(self.secondary_opts))

        help = self.help or ''
        extra = []

        if self.default is not None:
            if isinstance(self.default, (list, tuple)):
                default_string = ', '.join('%s' % d for d in self.default)
            elif _inspect.isfunction(self.default):
                default_string = "(dynamic)"
            else:
                default_string = self.default
            extra.append('default: {}'.format(default_string))

        if self.required:
            extra.append('required')
        if extra:
            help = '%s[%s]' % (help and help + '  ' or '', '; '.join(extra))

        return ((any_prefix_is_slash and '; ' or ' / ').join(rv), help)


def command(name=None, cls=None, **attrs):
    return click.command(name=name, cls=cls or Command, **attrs)


def group(name=None, cls=None, **attrs):
    return click.group(name=name, cls=cls or Group, **attrs)


def argument(*param_decls, cls=None, **attrs):
    return click.argument(*param_decls, cls=cls or Argument, **attrs)

EDIT: Here's an example of what it looks like integrated with Flask.

I'd be happy to open a PR (leaving the current Click behaviour as default; different from the sample code above) if upstream is interested in supporting this feature officially.

Please do, I've also just been hit with this.

Would love to see this feature as well!

As the docs quoted earlier say, Click intentionally does not implement this.

I know this is closed, but I'll just +1 this feature request. It could be completely optional.

I'm making a project right now where this would be really helpful.

Was this page helpful?
0 / 5 - 0 ratings