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

Move --help option defaults from its class to its decorator #2840

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Unreleased
``Option.default`` if ``Option.is_flag`` is ``False``. This results in
``Option.default`` not needing to implement `__bool__`. :pr:`2829`
- Incorrect ``click.edit`` typing has been corrected. :pr:`2804`
- Fix setup of help option's defaults when using a custom class on its
decorator. Removes ``HelpOption``. :issue:`2832` :pr:`2840`

Version 8.1.8
-------------
Expand Down
1 change: 0 additions & 1 deletion src/click/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .decorators import confirmation_option as confirmation_option
from .decorators import group as group
from .decorators import help_option as help_option
from .decorators import HelpOption as HelpOption
from .decorators import make_pass_decorator as make_pass_decorator
from .decorators import option as option
from .decorators import pass_context as pass_context
Expand Down
33 changes: 17 additions & 16 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,25 +1014,26 @@ def get_help_option_names(self, ctx: Context) -> list[str]:
return list(all_names)

def get_help_option(self, ctx: Context) -> Option | None:
"""Returns the help option object."""
help_options = self.get_help_option_names(ctx)
"""Returns the help option object.

if not help_options or not self.add_help_option:
Unless ``add_help_option`` is ``False``.
"""
help_option_names = self.get_help_option_names(ctx)

if not help_option_names or not self.add_help_option:
return None

def show_help(ctx: Context, param: Parameter, value: str) -> None:
if value and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color)
ctx.exit()

return Option(
help_options,
is_flag=True,
is_eager=True,
expose_value=False,
callback=show_help,
help=_("Show this message and exit."),
)
# Avoid circular import.
from .decorators import help_option

def dummy_func() -> None:
pass

# Call @help_option decorator to produce an help option with proper
# defaults and attach it to the dummy function defined above.
help_option(*help_option_names)(dummy_func)

return dummy_func.__click_params__.pop() # type: ignore[no-any-return, attr-defined]

def make_parser(self, ctx: Context) -> _OptionParser:
"""Creates the underlying option parser for this command."""
Expand Down
38 changes: 12 additions & 26 deletions src/click/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import inspect
import typing as t
from collections import abc
from functools import update_wrapper
from gettext import gettext as _

Expand Down Expand Up @@ -525,41 +524,28 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
return option(*param_decls, **kwargs)


class HelpOption(Option):
def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Pre-configured ``--help`` option which immediately prints the help page
and exits the program.
"""

def __init__(
self,
param_decls: abc.Sequence[str] | None = None,
**kwargs: t.Any,
) -> None:
if not param_decls:
param_decls = ("--help",)

kwargs.setdefault("is_flag", True)
kwargs.setdefault("expose_value", False)
kwargs.setdefault("is_eager", True)
kwargs.setdefault("help", _("Show this message and exit."))
kwargs.setdefault("callback", self.show_help)

super().__init__(param_decls, **kwargs)
:param param_decls: One or more option names. Defaults to the single
value ``"--help"``.
:param kwargs: Extra arguments are passed to :func:`option`.
"""

@staticmethod
def show_help(ctx: Context, param: Parameter, value: bool) -> None:
"""Callback that print the help page on ``<stdout>`` and exits."""
if value and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color)
ctx.exit()

if not param_decls:
param_decls = ("--help",)

def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Decorator for the pre-configured ``--help`` option defined above.
kwargs.setdefault("is_flag", True)
kwargs.setdefault("expose_value", False)
kwargs.setdefault("is_eager", True)
kwargs.setdefault("help", _("Show this message and exit."))
kwargs.setdefault("callback", show_help)

:param param_decls: One or more option names. Defaults to the single
value ``"--help"``.
:param kwargs: Extra arguments are passed to :func:`option`.
"""
kwargs.setdefault("cls", HelpOption)
return option(*param_decls, **kwargs)
39 changes: 39 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,45 @@ def cmd2(testoption):
assert "you wont see me" not in result.output


@pytest.mark.parametrize("custom_class", (True, False))
@pytest.mark.parametrize(
("name_specs", "expected"),
(
(
("-h", "--help"),
" -h, --help Show this message and exit.\n",
),
(
("-h",),
" -h Show this message and exit.\n"
" --help Show this message and exit.\n",
),
(
("--help",),
" --help Show this message and exit.\n",
),
),
)
def test_help_option_custom_names_and_class(runner, custom_class, name_specs, expected):
class CustomHelpOption(click.Option):
pass

option_attrs = {}
if custom_class:
option_attrs["cls"] = CustomHelpOption

@click.command()
@click.help_option(*name_specs, **option_attrs)
def cmd():
pass

for arg in name_specs:
result = runner.invoke(cmd, [arg])
assert not result.exception
assert result.exit_code == 0
assert expected in result.output


def test_bool_flag_with_type(runner):
@click.command()
@click.option("--shout/--no-shout", default=False, type=bool)
Expand Down
Loading