Repository: pallets/click
Pull request #623 (2016-08-01 20:09:43)

Fix a couple of typos in the documentation
Assuming that `master` is the right base for those. If not, I can reapply to another branch or split them up.


marble: +1

Patch:
diff --git a/CHANGES b/CHANGES
index 88ab920..4b09dbf 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,11 +8,28 @@ Version 7.0
 
 (upcoming release with new features, release date to be decided)
 
+- The user is now presented with the available choices if prompt=True and
+  type=click.Choice in a click.option. The choices are listed within
+  parentthesis like 'Choose fruit (apple, orange): '.
 - The exception objects now store unicode properly.
 - Added the ability to hide commands and options from help.
 - Added Float Range in Types.
 - `secho`'s first argument can now be `None`, like in `echo`.
 - Usage errors now hint at the `--help` option.
+- ``launch`` now works properly under Cygwin. See #650.
+- `CliRunner.invoke` now may receive `args` as a string representing
+  a Unix shell command. See #664.
+
+Version 6.7
+-----------
+
+(bugfix release; released on January 6th 2017)
+
+- Make `click.progressbar` work with `codecs.open` files. See #637.
+- Fix bug in bash completion with nested subcommands. See #639.
+- Fix test runner not saving caller env correctly. See #644.
+- Fix handling of SIGPIPE. See #626
+- Deal with broken Windows environments such as Google App Engine's. See #711.
 
 Version 6.6
 -----------
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index b31d0e1..69d5594 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -35,6 +35,8 @@ Submitting patches
 - For features: Consider whether your feature would be a better fit for an
   `external package <http://click.pocoo.org/contrib/>`_
 
+- For bugfixes: Submit against the latest maintenance branch instead of master!
+
 Running the testsuite
 ---------------------
 
diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py
index d97b66a..d9d26d2 100644
--- a/click/_bashcomplete.py
+++ b/click/_bashcomplete.py
@@ -30,8 +30,8 @@ def get_completion_script(prog_name, complete_var):
 
 def resolve_ctx(cli, prog_name, args):
     ctx = cli.make_context(prog_name, args, resilient_parsing=True)
-    while ctx.args + ctx.protected_args and isinstance(ctx.command, MultiCommand):
-        a = ctx.args + ctx.protected_args
+    while ctx.protected_args + ctx.args and isinstance(ctx.command, MultiCommand):
+        a = ctx.protected_args + ctx.args
         cmd = ctx.command.get_command(ctx, a[0])
         if cmd is None:
             return None
diff --git a/click/_compat.py b/click/_compat.py
index 3e24a4e..312e729 100644
--- a/click/_compat.py
+++ b/click/_compat.py
@@ -8,6 +8,7 @@ from weakref import WeakKeyDictionary
 
 PY2 = sys.version_info[0] == 2
 WIN = sys.platform.startswith('win')
+CYGWIN = sys.platform.startswith('cygwin')
 DEFAULT_COLUMNS = 80
 
 
@@ -161,8 +162,16 @@ if PY2:
     #
     # This code also lives in _winconsole for the fallback to the console
     # emulation stream.
-    if WIN:
+    #
+    # There are also Windows environments where the `msvcrt` module is not
+    # available (which is why we use try-catch instead of the WIN variable
+    # here), such as the Google App Engine development server on Windows. In
+    # those cases there is just nothing we can do.
+    try:
         import msvcrt
+    except ImportError:
+        set_binary_mode = lambda x: x
+    else:
         def set_binary_mode(f):
             try:
                 fileno = f.fileno()
@@ -171,8 +180,6 @@ if PY2:
             else:
                 msvcrt.setmode(fileno, os.O_BINARY)
             return f
-    else:
-        set_binary_mode = lambda x: x
 
     def isidentifier(x):
         return _identifier_re.search(x) is not None
diff --git a/click/_termui_impl.py b/click/_termui_impl.py
index 9796a9f..cebf3bb 100644
--- a/click/_termui_impl.py
+++ b/click/_termui_impl.py
@@ -14,7 +14,8 @@ import sys
 import time
 import math
 from ._compat import _default_text_stdout, range_type, PY2, isatty, \
-     open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types
+     open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \
+     CYGWIN
 from .utils import echo
 from .exceptions import ClickException
 
@@ -31,7 +32,7 @@ def _length_hint(obj):
     """Returns the length hint of an object."""
     try:
         return len(obj)
-    except TypeError:
+    except (AttributeError, TypeError):
         try:
             get_hint = type(obj).__length_hint__
         except AttributeError:
@@ -478,6 +479,14 @@ def open_url(url, wait=False, locate=False):
             args = 'start %s "" "%s"' % (
                 wait and '/WAIT' or '', url.replace('"', ''))
         return os.system(args)
+    elif CYGWIN:
+        if locate:
+            url = _unquote_file(url)
+            args = 'cygstart "%s"' % (os.path.dirname(url).replace('"', ''))
+        else:
+            args = 'cygstart %s "%s"' % (
+                wait and '-w' or '', url.replace('"', ''))
+        return os.system(args)
 
     try:
         if locate:
diff --git a/click/_unicodefun.py b/click/_unicodefun.py
index 09a60c3..2e83311 100644
--- a/click/_unicodefun.py
+++ b/click/_unicodefun.py
@@ -114,6 +114,5 @@ def _verify_python3_env():
 
     raise RuntimeError('Click will abort further execution because Python 3 '
                        'was configured to use ASCII as encoding for the '
-                       'environment.  Either run this under Python 2 or '
-                       'consult http://click.pocoo.org/python3/ for '
-                       'mitigation steps.' + extra)
+                       'environment.  Consult http://click.pocoo.org/python3/'
+                       'for mitigation steps.' + extra)
diff --git a/click/core.py b/click/core.py
index cb43d53..18c1766 100644
--- a/click/core.py
+++ b/click/core.py
@@ -1,3 +1,4 @@
+import errno
 import os
 import sys
 from contextlib import contextmanager
@@ -705,6 +706,11 @@ class BaseCommand(object):
                     raise
                 e.show()
                 sys.exit(e.exit_code)
+            except IOError as e:
+                if e.errno == errno.EPIPE:
+                    sys.exit(1)
+                else:
+                    raise
         except Abort:
             if not standalone_mode:
                 raise
@@ -1455,7 +1461,7 @@ class Option(Parameter):
                  prompt=False, confirmation_prompt=False,
                  hide_input=False, is_flag=None, flag_value=None,
                  multiple=False, count=False, allow_from_autoenv=True,
-                 type=None, help=None, hidden=False, **attrs):
+                 type=None, help=None, hidden=False, show_choices=True, **attrs):
         default_is_missing = attrs.get('default', _missing) is _missing
         Parameter.__init__(self, param_decls, type=type, **attrs)
 
@@ -1501,6 +1507,7 @@ class Option(Parameter):
         self.allow_from_autoenv = allow_from_autoenv
         self.help = help
         self.show_default = show_default
+        self.show_choices = show_choices
 
         # Sanity check for stuff we don't support
         if __debug__:
@@ -1652,8 +1659,8 @@ class Option(Parameter):
         if self.is_bool_flag:
             return confirm(self.prompt, default)
 
-        return prompt(self.prompt, default=default,
-                      hide_input=self.hide_input,
+        return prompt(self.prompt, default=default, type=self.type,
+                      hide_input=self.hide_input, show_choices=self.show_choices,
                       confirmation_prompt=self.confirmation_prompt,
                       value_proc=lambda x: self.process_value(ctx, x))
 
diff --git a/click/termui.py b/click/termui.py
index c8e42be..9a89afe 100644
--- a/click/termui.py
+++ b/click/termui.py
@@ -6,7 +6,7 @@ from ._compat import raw_input, text_type, string_types, \
      isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN
 from .utils import echo
 from .exceptions import Abort, UsageError
-from .types import convert_type
+from .types import convert_type, Choice
 from .globals import resolve_color_default
 
 
@@ -24,23 +24,27 @@ def hidden_prompt_func(prompt):
     return getpass.getpass(prompt)
 
 
-def _build_prompt(text, suffix, show_default=False, default=None):
+def _build_prompt(text, suffix, show_default=False, default=None, show_choices=True, type=None):
     prompt = text
+    if type is not None and show_choices and isinstance(type, Choice):
+        prompt += ' (' + ", ".join(map(str, type.choices)) + ')'
     if default is not None and show_default:
         prompt = '%s [%s]' % (prompt, default)
     return prompt + suffix
 
 
-def prompt(text, default=None, hide_input=False,
-           confirmation_prompt=False, type=None,
-           value_proc=None, prompt_suffix=': ',
-           show_default=True, err=False):
+def prompt(text, default=None, hide_input=False, confirmation_prompt=False,
+           type=None, value_proc=None, prompt_suffix=': ', show_default=True,
+           err=False, show_choices=True):
     """Prompts a user for input.  This is a convenience function that can
     be used to prompt a user for input later.
 
     If the user aborts the input by sending a interrupt signal, this
     function will catch it and raise a :exc:`Abort` exception.
 
+    .. versionadded:: 7.0
+       Added the show_choices parameter.
+
     .. versionadded:: 6.0
        Added unicode support for cmd.exe on Windows.
 
@@ -61,6 +65,10 @@ def prompt(text, default=None, hide_input=False,
     :param show_default: shows or hides the default value in the prompt.
     :param err: if set to true the file defaults to ``stderr`` instead of
                 ``stdout``, the same as with echo.
+    :param show_choices: Show or hide choices if the passed type is a Choice.
+                         For example if type is a Choice of either day or week,
+                         show_choices is true and text is "Group by" then the
+                         prompt will be "Group by (day, week): ".
     """
     result = None
 
@@ -82,7 +90,7 @@ def prompt(text, default=None, hide_input=False,
     if value_proc is None:
         value_proc = convert_type(type, default)
 
-    prompt = _build_prompt(text, prompt_suffix, show_default, default)
+    prompt = _build_prompt(text, prompt_suffix, show_default, default, show_choices, type)
 
     while 1:
         while 1:
diff --git a/click/types.py b/click/types.py
index dee1086..3eb5fac 100644
--- a/click/types.py
+++ b/click/types.py
@@ -425,7 +425,7 @@ class Path(ParamType):
         if self.file_okay and not self.dir_okay:
             self.name = 'file'
             self.path_type = 'File'
-        if self.dir_okay and not self.file_okay:
+        elif self.dir_okay and not self.file_okay:
             self.name = 'directory'
             self.path_type = 'Directory'
         else:
diff --git a/docs/advanced.rst b/docs/advanced.rst
index a0165f0..17f86da 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -277,7 +277,7 @@ options:
     If you go with this solution, the extra arguments will be collected in
     :attr:`Context.args`.
 2.  You can attach a :func:`argument` with ``nargs`` set to `-1` which
-    will eat up all leftover arguments.  In this case it's recommeded to
+    will eat up all leftover arguments.  In this case it's recommended to
     set the `type` to :data:`UNPROCESSED` to avoid any string processing
     on those arguments as otherwise they are forced into unicode strings
     automatically which is often not what you want.
diff --git a/docs/bashcomplete.rst b/docs/bashcomplete.rst
index 9154dfb..257bcff 100644
--- a/docs/bashcomplete.rst
+++ b/docs/bashcomplete.rst
@@ -42,7 +42,7 @@ with dashes replaced by underscores.
 
 If your tool is called ``foo-bar``, then the magic variable is called
 ``_FOO_BAR_COMPLETE``.  By exporting it with the ``source`` value it will
-spit out the activation script which can be trivally activated.
+spit out the activation script which can be trivially activated.
 
 For instance, to enable Bash completion for your ``foo-bar`` script, this
 is what you would need to put into your ``.bashrc``::
diff --git a/docs/commands.rst b/docs/commands.rst
index 29c71e4..5a7ef79 100644
--- a/docs/commands.rst
+++ b/docs/commands.rst
@@ -437,7 +437,7 @@ you're not satisfied with the defaults.
 
 The default map can be nested arbitrarily for each subcommand and
 provided when the script is invoked.  Alternatively, it can also be
-overriden at any point by commands.  For instance, a top-level command could
+overridden at any point by commands.  For instance, a top-level command could
 load the defaults from a configuration file.
 
 Example usage:
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index b682d21..7c4cccd 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -44,7 +44,7 @@ properly by this function.
 Multicommand Chaining API
 `````````````````````````
 
-Click 3 introduced multicommand chaning.  This required a change in how
+Click 3 introduced multicommand chaining.  This required a change in how
 Click internally dispatches.  Unfortunately this change was not correctly
 implemented and it appeared that it was possible to provide an API that
 can inform the super command about all the subcommands that will be
diff --git a/docs/utils.rst b/docs/utils.rst
index 2ecbe88..9bd98b0 100644
--- a/docs/utils.rst
+++ b/docs/utils.rst
@@ -226,7 +226,7 @@ Launching Applications
 .. versionadded:: 2.0
 
 Click supports launching applications through :func:`launch`.  This can be
-used to open the default application assocated with a URL or filetype.
+used to open the default application associated with a URL or filetype.
 This can be used to launch web browsers or picture viewers, for instance.
 In addition to this, it can also launch the file manager and automatically
 select the provided file.
diff --git a/setup.cfg b/setup.cfg
index 7c964b4..947f15f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,5 @@
 [wheel]
 universal=1
+
+[metadata]
+license_file = LICENSE


Test patch:
diff --git a/click/testing.py b/click/testing.py
index d43581f..09d37a7 100644
--- a/click/testing.py
+++ b/click/testing.py
@@ -3,8 +3,9 @@ import sys
 import shutil
 import tempfile
 import contextlib
+import shlex
 
-from ._compat import iteritems, PY2
+from ._compat import iteritems, PY2, string_types
 
 
 # If someone wants to vendor click, we want to ensure the
@@ -213,7 +214,7 @@ class CliRunner(object):
         old_env = {}
         try:
             for key, value in iteritems(env):
-                old_env[key] = os.environ.get(value)
+                old_env[key] = os.environ.get(key)
                 if value is None:
                     try:
                         del os.environ[key]
@@ -260,7 +261,10 @@ class CliRunner(object):
            The ``color`` parameter was added.
 
         :param cli: the command to invoke
-        :param args: the arguments to invoke
+        :param args: the arguments to invoke. It may be given as an iterable
+                     or a string. When given as string it will be interpreted
+                     as a Unix shell command. More details at
+                     :func:`shlex.split`.
         :param input: the input data for `sys.stdin`.
         :param env: the environment overrides.
         :param catch_exceptions: Whether to catch any other exceptions than
@@ -274,6 +278,9 @@ class CliRunner(object):
             exception = None
             exit_code = 0
 
+            if isinstance(args, string_types):
+                args = shlex.split(args)
+
             try:
                 cli.main(args=args or (),
                          prog_name=self.get_default_prog_name(cli), **extra)
diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py
index f75a236..fea096c 100644
--- a/tests/test_bashcomplete.py
+++ b/tests/test_bashcomplete.py
@@ -3,7 +3,18 @@
 import click
 from click._bashcomplete import get_choices
 
-def test_basic():
+
+def test_single_command():
+    @click.command()
+    @click.option('--local-opt')
+    def cli(local_opt):
+        pass
+
+    assert list(get_choices(cli, 'lol', [], '-')) == ['--local-opt']
+    assert list(get_choices(cli, 'lol', [], '')) == []
+
+
+def test_small_chain():
     @click.group()
     @click.option('--global-opt')
     def cli(global_opt):
@@ -18,3 +29,34 @@ def test_basic():
     assert list(get_choices(cli, 'lol', [], '-')) == ['--global-opt']
     assert list(get_choices(cli, 'lol', ['sub'], '')) == []
     assert list(get_choices(cli, 'lol', ['sub'], '-')) == ['--local-opt']
+
+
+def test_long_chain():
+    @click.group('cli')
+    @click.option('--cli-opt')
+    def cli(cli_opt):
+        pass
+
+    @cli.group('asub')
+    @click.option('--asub-opt')
+    def asub(asub_opt):
+        pass
+
+    @asub.group('bsub')
+    @click.option('--bsub-opt')
+    def bsub(bsub_opt):
+        pass
+
+    @bsub.command('csub')
+    @click.option('--csub-opt')
+    def csub(csub_opt):
+        pass
+
+    assert list(get_choices(cli, 'lol', [], '-')) == ['--cli-opt']
+    assert list(get_choices(cli, 'lol', [], '')) == ['asub']
+    assert list(get_choices(cli, 'lol', ['asub'], '-')) == ['--asub-opt']
+    assert list(get_choices(cli, 'lol', ['asub'], '')) == ['bsub']
+    assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-opt']
+    assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '')) == ['csub']
+    assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '-')) == ['--csub-opt']
+    assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '')) == []
diff --git a/tests/test_imports.py b/tests/test_imports.py
index 6a8a122..bc54533 100644
--- a/tests/test_imports.py
+++ b/tests/test_imports.py
@@ -32,7 +32,7 @@ click.echo(json.dumps(rv))
 ALLOWED_IMPORTS = set([
     'weakref', 'os', 'struct', 'collections', 'sys', 'contextlib',
     'functools', 'stat', 're', 'codecs', 'inspect', 'itertools', 'io',
-    'threading', 'colorama'
+    'threading', 'colorama', 'errno'
 ])
 
 if WIN:
diff --git a/tests/test_termui.py b/tests/test_termui.py
index fa7c262..7806c40 100644
--- a/tests/test_termui.py
+++ b/tests/test_termui.py
@@ -44,6 +44,26 @@ def test_progressbar_length_hint(runner, monkeypatch):
     assert result.exception is None
 
 
+def test_choices_list_in_prompt(runner, monkeypatch):
+    @click.command()
+    @click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']),
+                  prompt=True)
+    def cli_with_choices(g):
+        pass
+
+    @click.command()
+    @click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']),
+                  prompt=True, show_choices=False)
+    def cli_without_choices(g):
+        pass
+
+    result = runner.invoke(cli_with_choices, [], input='none')
+    assert '(none, day, week, month)' in result.output
+
+    result = runner.invoke(cli_without_choices, [], input='none')
+    assert '(none, day, week, month)' not in result.output
+
+
 def test_secho(runner):
     with runner.isolation() as out:
         click.secho(None, nl=False)
diff --git a/tests/test_testing.py b/tests/test_testing.py
index fab2875..c091095 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -1,3 +1,4 @@
+import os
 import sys
 
 import pytest
@@ -183,3 +184,41 @@ def test_exit_code_and_output_from_sys_exit():
     result = runner.invoke(cli_no_error)
     assert result.exit_code == 0
     assert result.output == 'hello world\n'
+
+
+def test_env():
+    @click.command()
+    def cli_env():
+        click.echo('ENV=%s' % os.environ['TEST_CLICK_ENV'])
+
+    runner = CliRunner()
+
+    env_orig = dict(os.environ)
+    env = dict(env_orig)
+    assert 'TEST_CLICK_ENV' not in env
+    env['TEST_CLICK_ENV'] = 'some_value'
+    result = runner.invoke(cli_env, env=env)
+    assert result.exit_code == 0
+    assert result.output == 'ENV=some_value\n'
+
+    assert os.environ == env_orig
+
+
+@pytest.mark.parametrize('args, expected_output', [
+    (None, 'bar\n'),
+    ([], 'bar\n'),
+    ('', 'bar\n'),
+    (['--foo', 'one two'], 'one two\n'),
+    ('--foo "one two"', 'one two\n'),
+])
+def test_args(args, expected_output):
+
+    @click.command()
+    @click.option('--foo', default='bar')
+    def cli_args(foo):
+        click.echo(foo)
+
+    runner = CliRunner()
+    result = runner.invoke(cli_args, args=args)
+    assert result.exit_code == 0
+    assert result.output == expected_output