Repository: pallets/click
Pull request #920 (2018-01-29 15:41:28)

Use the realpath of original filename in _AtomicFile
This way, the code becomes resilient to the chdir() changing mid-way, between
the open() and close() of _AtomicFile.


Patch:
diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml
index 1d81f1e..caa509a 100644
--- a/.ci/appveyor.yml
+++ b/.ci/appveyor.yml
@@ -32,6 +32,22 @@ environment:
       PYTHON_VERSION: "3.4.x"
       PYTHON_ARCH: "64"
 
+    - PYTHON: "C:\\Python35"
+      PYTHON_VERSION: "3.5.x"
+      PYTHON_ARCH: "32"
+
+    - PYTHON: "C:\\Python35-x64"
+      PYTHON_VERSION: "3.5.x"
+      PYTHON_ARCH: "64"
+
+    - PYTHON: "C:\\Python36"
+      PYTHON_VERSION: "3.6.x"
+      PYTHON_ARCH: "32"
+
+    - PYTHON: "C:\\Python36-x64"
+      PYTHON_VERSION: "3.6.x"
+      PYTHON_ARCH: "64"
+
 branches:
   only:
     - master
diff --git a/.gitignore b/.gitignore
index 2035180..ed66c15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 *.pyo
 *.egg-ignore
 *.egg-info
+.pytest_cache
 dist
 build
 docs/_build
diff --git a/CHANGES b/CHANGES
index 5e8c57c..a44f322 100644
--- a/CHANGES
+++ b/CHANGES
@@ -31,6 +31,9 @@ Version 7.0
   See #790.
 - Allow autocompletion function to determine whether or not to return
   completions that start with the incomplete argument.
+- Subcommands that are named by the function now automatically have the
+  underscore replaced with a dash.  So if you register a function named
+  `my_command` it becomes `my-command` in the command line interface.
 
 Version 6.8
 -----------
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 69d5594..99b493a 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -41,7 +41,7 @@ Running the testsuite
 ---------------------
 
 You probably want to set up a `virtualenv
-<http://virtualenv.readthedocs.org/en/latest/index.html>`_.
+<https://virtualenv.readthedocs.io/en/latest/index.html>`_.
 
 The minimal requirement for running the testsuite is ``py.test``.  You can
 install it with::
diff --git a/click/__init__.py b/click/__init__.py
index 58df503..6de314b 100644
--- a/click/__init__.py
+++ b/click/__init__.py
@@ -66,7 +66,7 @@ __all__ = [
 
     # Types
     'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple', 'STRING',
-    'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', 'FloatRange'
+    'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', 'FloatRange',
 
     # Utilities
     'echo', 'get_binary_stream', 'get_text_stream', 'open_file',
diff --git a/click/_compat.py b/click/_compat.py
index d072224..bc1273c 100644
--- a/click/_compat.py
+++ b/click/_compat.py
@@ -167,11 +167,12 @@ if PY2:
     # 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.
+    def set_binary_mode(f):
+        return f
+
     try:
         import msvcrt
-    except ImportError:
-        set_binary_mode = lambda x: x
-    else:
+
         def set_binary_mode(f):
             try:
                 fileno = f.fileno()
@@ -180,6 +181,22 @@ if PY2:
             else:
                 msvcrt.setmode(fileno, os.O_BINARY)
             return f
+    except ImportError:
+        pass
+
+    try:
+        def set_binary_mode(f):
+            try:
+                fileno = f.fileno()
+            except Exception:
+                pass
+            else:
+                import fcntl
+                flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
+                fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
+            return f
+    except ImportError:
+        pass
 
     def isidentifier(x):
         return _identifier_re.search(x) is not None
@@ -423,7 +440,7 @@ def open_stream(filename, mode='r', encoding=None, errors='strict',
     # Standard streams first.  These are simple because they don't need
     # special handling for the atomic flag.  It's entirely ignored.
     if filename == '-':
-        if 'w' in mode:
+        if any(m in mode for m in ['w', 'a', 'x']):
             if 'b' in mode:
                 return get_binary_stdout(), False
             return get_text_stdout(encoding=encoding, errors=errors), False
@@ -463,7 +480,7 @@ def open_stream(filename, mode='r', encoding=None, errors='strict',
     else:
         f = os.fdopen(fd, mode)
 
-    return _AtomicFile(f, tmp_filename, filename), True
+    return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True
 
 
 # Used in a destructor call, needs extra protection from interpreter cleanup.
diff --git a/click/core.py b/click/core.py
index 8358172..25155d6 100644
--- a/click/core.py
+++ b/click/core.py
@@ -59,9 +59,7 @@ def _check_multicommand(base_command, cmd_name, cmd, register=False):
     raise RuntimeError('%s.  Command "%s" is set to chain and "%s" was '
                        'added as subcommand but it in itself is a '
                        'multi command.  ("%s" is a %s within a chained '
-                       '%s named "%s").  This restriction was supposed to '
-                       'be lifted in 6.0 but the fix was flawed.  This '
-                       'will be fixed in Click 7.0' % (
+                       '%s named "%s").' % (
                            hint, base_command.name, cmd_name,
                            cmd_name, cmd.__class__.__name__,
                            base_command.__class__.__name__,
diff --git a/click/decorators.py b/click/decorators.py
index c0b9e85..8e5a462 100644
--- a/click/decorators.py
+++ b/click/decorators.py
@@ -85,7 +85,7 @@ def _make_command(f, name, attrs, cls):
         help = inspect.cleandoc(help)
     attrs['help'] = help
     _check_for_unicode_literals()
-    return cls(name=name or f.__name__.lower(),
+    return cls(name=name or f.__name__.lower().replace('_', '-'),
                callback=f, params=params, **attrs)
 
 
@@ -105,7 +105,7 @@ def command(name=None, cls=None, **attrs):
     command :class:`Group`.
 
     :param name: the name of the command.  This defaults to the function
-                 name.
+                 name with underscores replaced by dashes.
     :param cls: the command class to instantiate.  This defaults to
                 :class:`Command`.
     """
diff --git a/docs/commands.rst b/docs/commands.rst
index 5a7ef79..2320432 100644
--- a/docs/commands.rst
+++ b/docs/commands.rst
@@ -29,7 +29,7 @@ when an inner command runs:
 
     @cli.command()
     def sync():
-        click.echo('Synching')
+        click.echo('Syncing')
 
 Here is what this looks like:
 
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 8d02296..234f809 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -51,7 +51,7 @@ If you are on Windows (or none of the above methods worked) you must install
 Once you have it installed, run the ``pip`` command from above, but without
 the `sudo` prefix.
 
-.. _installing pip: http://pip.readthedocs.org/en/latest/installing.html
+.. _installing pip: https://pip.readthedocs.io/en/latest/installing.html
 
 Once you have virtualenv installed, just fire up a shell and create
 your own environment.  I usually create a project folder and a `venv`
diff --git a/examples/validation/validation.py b/examples/validation/validation.py
index 4b95091..00fa0a6 100644
--- a/examples/validation/validation.py
+++ b/examples/validation/validation.py
@@ -1,6 +1,6 @@
 import click
 try:
-    from urllib import parser as urlparse
+    from urllib import parse as urlparse
 except ImportError:
     import urlparse
 


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 9b6a6fb..e900c43 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -62,7 +62,7 @@ def test_auto_shorthelp(runner):
         r'Commands:\n\s+'
         r'long\s+This is a long text that is too long to show\.\.\.\n\s+'
         r'short\s+This is a short text\.\n\s+'
-        r'special_chars\s+Login and store the token in ~/.netrc\.\s*',
+        r'special-chars\s+Login and store the token in ~/.netrc\.\s*',
         result.output) is not None
 
 
@@ -206,7 +206,7 @@ def test_other_command_invoke_with_defaults(runner):
     @click.option('--foo', type=click.INT, default=42)
     @click.pass_context
     def other_cmd(ctx, foo):
-        assert ctx.info_name == 'other_cmd'
+        assert ctx.info_name == 'other-cmd'
         click.echo(foo)
 
     result = runner.invoke(cli, [])
@@ -253,3 +253,17 @@ def test_unprocessed_options(runner):
         'Verbosity: 4',
         'Args: -foo|-x|--muhaha|x|y|-x',
     ]
+
+
+def test_subcommand_naming(runner):
+    @click.group()
+    def cli():
+        pass
+
+    @cli.command()
+    def foo_bar():
+        click.echo('foo-bar')
+
+    result = runner.invoke(cli, ['foo-bar'])
+    assert not result.exception
+    assert result.output.splitlines() == ['foo-bar']
diff --git a/tests/test_formatting.py b/tests/test_formatting.py
index 4c1c491..b3de277 100644
--- a/tests/test_formatting.py
+++ b/tests/test_formatting.py
@@ -74,11 +74,11 @@ def test_wrapping_long_options_strings(runner):
 
     # 54 is chosen as a length where the second line is one character
     # longer than the maximum length.
-    result = runner.invoke(cli, ['a_very_long', 'command', '--help'],
+    result = runner.invoke(cli, ['a-very-long', 'command', '--help'],
                            terminal_width=54)
     assert not result.exception
     assert result.output.splitlines() == [
-        'Usage: cli a_very_long command [OPTIONS] FIRST SECOND',
+        'Usage: cli a-very-long command [OPTIONS] FIRST SECOND',
         '                               THIRD FOURTH FIFTH',
         '                               SIXTH',
         '',
@@ -111,11 +111,11 @@ def test_wrapping_long_command_name(runner):
         """A command.
         """
 
-    result = runner.invoke(cli, ['a_very_very_very_long', 'command', '--help'],
+    result = runner.invoke(cli, ['a-very-very-very-long', 'command', '--help'],
                            terminal_width=54)
     assert not result.exception
     assert result.output.splitlines() == [
-        'Usage: cli a_very_very_very_long command ',
+        'Usage: cli a-very-very-very-long command ',
         '           [OPTIONS] FIRST SECOND THIRD FOURTH FIFTH',
         '           SIXTH',
         '',