From 985ca1651be08747ce713eb52568f8c8a932131f Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 19 May 2021 11:35:07 -0700 Subject: [PATCH] show help text with invalid default --- CHANGES.rst | 2 ++ src/click/core.py | 28 ++++++++++++++++++++++++---- tests/test_basic.py | 17 +++++++++++++++++ tests/test_options.py | 2 +- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6d807d18d..9bc6e54af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -25,6 +25,8 @@ Unreleased ``default_map`` lookups. When using patterns like ``AliasedGroup``, override ``resolve_command`` to change the name that is returned if needed. :issue:`1895` +- If a default value is invalid, it does not prevent showing help + text. :issue:`1889` Version 8.0.0 diff --git a/src/click/core.py b/src/click/core.py index 3a7533bea..7000cffc1 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -2196,6 +2196,10 @@ def get_default( :param call: If the default is a callable, call it. Disable to return the callable instead. + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + .. versionchanged:: 8.0 Looks at ``ctx.default_map`` first. @@ -2214,7 +2218,13 @@ def get_default( value = value() - return self.type_cast_value(ctx, value) + try: + return self.type_cast_value(ctx, value) + except BadParameter: + if ctx.resilient_parsing: + return value + + raise def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: raise NotImplementedError() @@ -2700,14 +2710,24 @@ def _write_opts(opts: t.Sequence[str]) -> str: ) extra.append(_("env var: {var}").format(var=var_str)) - default_value = self.get_default(ctx, call=False) + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + show_default_is_str = isinstance(self.show_default, str) if show_default_is_str or ( default_value is not None and (self.show_default or ctx.show_default) ): if show_default_is_str: - default_string: t.Union[str, t.Any] = f"({self.show_default})" + default_string = f"({self.show_default})" elif isinstance(default_value, (list, tuple)): default_string = ", ".join(str(d) for d in default_value) elif callable(default_value): @@ -2719,7 +2739,7 @@ def _write_opts(opts: t.Sequence[str]) -> str: (self.opts if self.default else self.secondary_opts)[0] )[1] else: - default_string = default_value + default_string = str(default_value) extra.append(_("default: {default}").format(default=default_string)) diff --git a/tests/test_basic.py b/tests/test_basic.py index c35e69ad1..c38c1afd4 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -547,3 +547,20 @@ def cmd(): result = runner.invoke(cli, ["--help"]) assert "Summary line without period" in result.output assert "Here is a sentence." not in result.output + + +def test_help_invalid_default(runner): + cli = click.Command( + "cli", + params=[ + click.Option( + ["-a"], + type=click.Path(exists=True), + default="not found", + show_default=True, + ), + ], + ) + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + assert "default: not found" in result.output diff --git a/tests/test_options.py b/tests/test_options.py index 50e6b5ab0..323d1c422 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -553,7 +553,7 @@ def cmd(config): def test_argument_custom_class(runner): class CustomArgument(click.Argument): - def get_default(self, ctx): + def get_default(self, ctx, call=True): """a dumb override of a default value for testing""" return "I am a default"