Skip to content

Commit

Permalink
Merge pull request #1916 from pallets/default-resilient
Browse files Browse the repository at this point in the history
show help text with invalid default
  • Loading branch information
davidism committed May 19, 2021
2 parents 1b49159 + 985ca16 commit fcf8205
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -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
Expand Down
28 changes: 24 additions & 4 deletions src/click/core.py
Expand Up @@ -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.
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand All @@ -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))

Expand Down
17 changes: 17 additions & 0 deletions tests/test_basic.py
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/test_options.py
Expand Up @@ -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"

Expand Down

0 comments on commit fcf8205

Please sign in to comment.