Skip to content
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

add help to click argument #587

Closed
borismod opened this issue May 19, 2016 · 7 comments
Closed

add help to click argument #587

borismod opened this issue May 19, 2016 · 7 comments

Comments

@borismod
Copy link

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:\Python27\lib\runpy.py", line 162, in run_module_as_main
"main", fname, loader, pkg_name)
File "C:\Python27\lib\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:\Python27\lib\site-packages\click\decorators.py", line 151, in decorator
_param_memo(f, ArgumentClass(param_decls, **attrs))
File "C:\Python27\lib\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'

@tony
Copy link

tony commented May 31, 2016

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?

@Evidlo
Copy link

Evidlo commented Feb 6, 2018

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.

@briancappello
Copy link

briancappello commented Aug 2, 2018

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.

@hadsed

This comment has been minimized.

@Nukesor

This comment has been minimized.

@davidism
Copy link
Member

davidism commented Sep 9, 2018

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

@davidism davidism closed this as completed Sep 9, 2018
@vesche

This comment has been minimized.

@pallets pallets locked as resolved and limited conversation to collaborators Oct 30, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants