Repository: pallets/click

Issue:
show_default attempts to print lambdas when using dynamic defaults
Given the code:

```python
#!/usr/bin/env python

import click
from datetime import date


@click.command()
@click.option('--day', help="choose the day of the week",
              default=lambda: date.today().strftime("%A"), show_default=True)
def main(day):
    print "Hello world! It's a wonderful {}.".format(day)


if __name__ == '__main__':
    main()
```

We get the correct default value when running the script:

```
$ ./hello.py
Hello world! It's a wonderful Tuesday.
```

However, the help text tries to print the lambda:

```
$ ./hello.py --help
Usage: hello.py [OPTIONS]

Options:
  --day TEXT  choose the day of the week  [default: <function <lambda> at
              0x109c028c0>]
  --help      Show this message and exit.
```

default=lambda and show_default behavior
This:
`@click.option('--username',  callback=set_option_value, default=lambda: get_option_default('username'), show_default=True)`

gives this --help output:
`--username TEXT                 [default: <function <lambda> at  0x104858a28>]`

If that is the intended behavior, is there an option to have it invoke the function during --help?



Discussion:
I see. We should prevent usage of `show_default=True` when the default is dynamic. The correct way to document a dynamic default is to describe it in the help text IMO (not evaluate it).
Is there any downside to evaluating it?

I couldn't see any way of defining a dynamic help text, which is what I'd want in this case if click didn't evaluate my default.
Why would you want to evaluate it? That is less informative than describing how this value is calculated.
So users can see the current value. I use this for expanding `~/foo` to `/home/wilfred/foo`, and I'd like to show the path in my help message.

Click can't understand what an arbitrary lambda expression does, and I can already describe how it's calculated in my argument summary.
If we do this then we need to at least clearly signal that this default is dynamically calculated.

On 31 August 2017 16:25:48 GMT+02:00, Wilfred Hughes <notifications@github.com> wrote:
>So users can see the current value. I use this for expanding `~/foo` to
>`/home/wilfred/foo`, and I'd like to show the path in my help message.
>
>Click can't understand what an arbitrary lambda expression does, and I
>can already describe how it's calculated in my argument summary.
>
>-- 
>You are receiving this because you commented.
>Reply to this email directly or view it on GitHub:
>https://github.com/pallets/click/issues/844#issuecomment-326312030

-- 
Sent from my Android device with K-9 Mail. Please excuse my brevity.
Duplicate of https://github.com/pallets/click/issues/547.
The current behavior is a bug, but I'm unsure whether it's a good idea to just inline the return value of the lambda? IMO it's better to describe the default callback's logic in the option helptext.

I think I agree but it would also be nice to have the option for --help to invoke the function. 

JFI: the code in question: https://github.com/pallets/click/blob/defe6aecef0466b7eecc4ffa3f7c18c72c0d5192/click/core.py#L1635-L1639
In #844, the suggestion is made to evaluate the default value when running `--help`. This seems possibly dangerous or ripe for exploitation if the `lambda` on the option ran something which updates a database, writes to a file, etc.

Instead, I'm working on a fix for this that adds a new optional parameter to `@click.option` to be used to give a description of what the dynamic value would be.

Resolving patch:
diff --git a/click/core.py b/click/core.py
--- a/click/core.py
+++ b/click/core.py
@@ -1,4 +1,5 @@
 import errno
+import inspect
 import os
 import sys
 from contextlib import contextmanager
@@ -14,7 +15,7 @@
 from .parser import OptionParser, split_opt
 from .globals import push_context, pop_context
 
-from ._compat import PY2, isidentifier, iteritems
+from ._compat import PY2, isidentifier, iteritems, string_types
 from ._unicodefun import _check_for_unicode_literals, _verify_python3_env
 
 
@@ -1444,7 +1445,9 @@ class Option(Parameter):
     All other parameters are passed onwards to the parameter constructor.
 
     :param show_default: controls if the default value should be shown on the
-                         help page.  Normally, defaults are not shown.
+                         help page. Normally, defaults are not shown. If this
+                         value is a string, it shows the string instead of the
+                         value. This is particularly useful for dynamic options.
     :param show_envvar: controls if an environment variable should be shown on
                         the help page.  Normally, environment variables
                         are not shown.
@@ -1653,10 +1656,16 @@ def _write_opts(opts):
                            if isinstance(envvar, (list, tuple))
                            else envvar, ))
         if self.default is not None and self.show_default:
-            extra.append('default: %s' % (
-                         ', '.join('%s' % d for d in self.default)
-                         if isinstance(self.default, (list, tuple))
-                         else self.default, ))
+            if isinstance(self.show_default, string_types):
+                default_string = '({})'.format(self.show_default)
+            elif 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:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -24,14 +24,20 @@
     maintainer_email='contact@palletsprojects.com',
     long_description=readme,
     packages=['click'],
+    description='A simple wrapper around optparse for '
+                'powerful command line utilities.',
+    license='BSD',
     extras_require={
-        'docs': [
+        'dev': [
+            'pytest>=3',
+            'coverage',
+            'tox',
             'sphinx',
         ],
+        'docs': [
+            'sphinx',
+        ]
     },
-    description='A simple wrapper around optparse for '
-                'powerful command line utilities.',
-    license='BSD',
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Intended Audience :: Developers',


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -174,6 +174,33 @@ def cmd(arg1, arg2):
                            'two --arg2 4 four'.split())
     assert not result.exception
 
+def test_dynamic_default_help_unset(runner):
+    @click.command()
+    @click.option('--username', prompt=True,
+                  default=lambda: os.environ.get('USER', ''),
+                  show_default=True)
+    def cmd(username):
+        print("Hello,", username)
+
+    result = runner.invoke(cmd, ['--help'])
+    assert result.exit_code == 0
+    assert '--username' in result.output
+    assert 'lambda' not in result.output
+    assert '(dynamic)' in result.output
+
+def test_dynamic_default_help_text(runner):
+    @click.command()
+    @click.option('--username', prompt=True,
+                  default=lambda: os.environ.get('USER', ''),
+                  show_default='current user')
+    def cmd(username):
+        print("Hello,", username)
+
+    result = runner.invoke(cmd, ['--help'])
+    assert result.exit_code == 0
+    assert '--username' in result.output
+    assert 'lambda' not in result.output
+    assert '(current user)' in result.output
 
 def test_nargs_envvar(runner):
     @click.command()
@@ -302,7 +329,6 @@ def cmd(foo):
     assert '              i am' in out
     assert '              multiline' in out
 
-
 def test_argument_custom_class(runner):
     class CustomArgument(click.Argument):
         def get_default(self, ctx):