Skip to content

Commit

Permalink
Allow customization of help option right from its decorator.
Browse files Browse the repository at this point in the history
Removes `click.decorators.HelpOption` class.

Closes #2832.
  • Loading branch information
kdeldycke committed Jan 9, 2025
1 parent 8a47580 commit 34d2533
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 43 deletions.
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

0 comments on commit 34d2533

Please sign in to comment.