Repository: cookiecutter/cookiecutter

Issue:
Consider migrating to http://click.pocoo.org for the CLI
Besides the much nicer interface, it also:
- introduces no additional downstream dependencies
- supports the same pythons we do (https://github.com/mitsuhiko/click/blob/master/tox.ini#L2)
- ships with some awesome utilities (http://click.pocoo.org/3/utils/)



Discussion:
OMG yes. I've wanted click in cookiecutter since it first came out, but never made a ticket for it.

:+1: for using click!

:+1: I'm using click for a wrapper library, I'm really liking it. 


Resolving patch:
diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py
new file mode 100755
--- /dev/null
+++ b/cookiecutter/cli.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+cookiecutter.cli
+-----------------
+
+Main `cookiecutter` CLI.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import sys
+import logging
+
+import click
+
+from cookiecutter import __version__
+from cookiecutter.main import cookiecutter
+
+logger = logging.getLogger(__name__)
+
+
+def print_version(context, param, value):
+    if not value or context.resilient_parsing:
+        return
+    click.echo('Cookiecutter %s from %s (Python %s)' % (
+        __version__,
+        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+        sys.version[:3]
+    ))
+    context.exit()
+
+
+@click.command()
+@click.argument('template')
+@click.option(
+    '--no-input', is_flag=True,
+    help='Do not prompt for parameters and only use cookiecutter.json '
+         'file content',
+)
+@click.option(
+    '-c', '--checkout',
+    help='branch, tag or commit to checkout after git clone',
+)
+@click.option(
+    '-V', '--version',
+    is_flag=True, help='Show version information and exit.',
+    callback=print_version, expose_value=False, is_eager=True,
+)
+@click.option(
+    '-v', '--verbose',
+    is_flag=True, help='Print debug information', default=False
+)
+def main(template, no_input, checkout, verbose):
+    """Create a project from a Cookiecutter project template (TEMPLATE)."""
+    if verbose:
+        logging.basicConfig(
+            format='%(levelname)s %(filename)s: %(message)s',
+            level=logging.DEBUG
+        )
+    else:
+        # Log info and above to console
+        logging.basicConfig(
+            format='%(levelname)s: %(message)s',
+            level=logging.INFO
+        )
+
+    cookiecutter(template, checkout, no_input)
diff --git a/cookiecutter/main.py b/cookiecutter/main.py
--- a/cookiecutter/main.py
+++ b/cookiecutter/main.py
@@ -12,12 +12,9 @@
 """
 
 from __future__ import unicode_literals
-import argparse
 import logging
 import os
-import sys
 
-from . import __version__
 from .config import get_user_config
 from .prompt import prompt_for_config
 from .generate import generate_context, generate_files
@@ -31,11 +28,11 @@
 }
 
 
-def expand_abbreviations(input_dir, config_dict):
+def expand_abbreviations(template, config_dict):
     """
     Expand abbreviations in a template name.
 
-    :param input_dir: The project template name.
+    :param template: The project template name.
     :param config_dict: The user config, which will contain abbreviation
         definitions.
     """
@@ -43,23 +40,23 @@ def expand_abbreviations(input_dir, config_dict):
     abbreviations = builtin_abbreviations.copy()
     abbreviations.update(config_dict.get('abbreviations', {}))
 
-    if input_dir in abbreviations:
-        return abbreviations[input_dir]
+    if template in abbreviations:
+        return abbreviations[template]
 
     # Split on colon. If there is no colon, rest will be empty
-    # and prefix will be the whole input_dir
-    prefix, sep, rest = input_dir.partition(':')
+    # and prefix will be the whole template
+    prefix, sep, rest = template.partition(':')
     if prefix in abbreviations:
         return abbreviations[prefix].format(rest)
 
-    return input_dir
+    return template
 
 
-def cookiecutter(input_dir, checkout=None, no_input=False, extra_context=None):
+def cookiecutter(template, checkout=None, no_input=False, extra_context=None):
     """
     API equivalent to using Cookiecutter at the command line.
 
-    :param input_dir: A directory containing a project template dir,
+    :param template: A directory containing a project template directory,
         or a URL to a git repository.
     :param checkout: The branch, tag or commit ID to checkout after clone.
     :param no_input: Prompt the user at command line for manual configuration?
@@ -71,12 +68,12 @@ def cookiecutter(input_dir, checkout=None, no_input=False, extra_context=None):
     # If no config file, sensible defaults from config.DEFAULT_CONFIG are used
     config_dict = get_user_config()
 
-    input_dir = expand_abbreviations(input_dir, config_dict)
+    template = expand_abbreviations(template, config_dict)
 
     # TODO: find a better way to tell if it's a repo URL
-    if 'git@' in input_dir or 'https://' in input_dir:
+    if 'git@' in template or 'https://' in template:
         repo_dir = clone(
-            repo_url=input_dir,
+            repo_url=template,
             checkout=checkout,
             clone_to_dir=config_dict['cookiecutters_dir'],
             no_input=no_input
@@ -84,7 +81,7 @@ def cookiecutter(input_dir, checkout=None, no_input=False, extra_context=None):
     else:
         # If it's a local repo, no need to clone or copy to your
         # cookiecutters_dir
-        repo_dir = input_dir
+        repo_dir = template
 
     context_file = os.path.join(repo_dir, 'cookiecutter.json')
     logging.debug('context_file is {0}'.format(context_file))
@@ -104,70 +101,3 @@ def cookiecutter(input_dir, checkout=None, no_input=False, extra_context=None):
         repo_dir=repo_dir,
         context=context
     )
-
-
-def _get_parser():
-    parser = argparse.ArgumentParser(
-        description='Create a project from a Cookiecutter project template.'
-    )
-    parser.add_argument(
-        '--no-input',
-        action='store_true',
-        help='Do not prompt for parameters and only use cookiecutter.json '
-             'file content')
-    parser.add_argument(
-        'input_dir',
-        help='Cookiecutter project dir, e.g. cookiecutter-pypackage/'
-    )
-    parser.add_argument(
-        '-c', '--checkout',
-        help='branch, tag or commit to checkout after git clone'
-    )
-    cookiecutter_pkg_dir = os.path.dirname(
-        os.path.dirname(os.path.abspath(__file__))
-    )
-    parser.add_argument(
-        '-V', '--version',
-        help='Show version information and exit.',
-        action='version',
-        version='Cookiecutter {0} from {1} (Python {2})'.format(
-            __version__,
-            cookiecutter_pkg_dir,
-            sys.version[:3]
-        )
-    )
-    parser.add_argument(
-        '-v', '--verbose',
-        help='Print debug information',
-        action='store_true', default=False
-    )
-
-    return parser
-
-
-def parse_cookiecutter_args(args):
-    """ Parse the command-line arguments to Cookiecutter. """
-    parser = _get_parser()
-    return parser.parse_args(args)
-
-
-def main():
-    """ Entry point for the package, as defined in setup.py. """
-
-    args = parse_cookiecutter_args(sys.argv[1:])
-
-    if args.verbose:
-        logging.basicConfig(format='%(levelname)s %(filename)s: %(message)s',
-                            level=logging.DEBUG)
-    else:
-        # Log info and above to console
-        logging.basicConfig(
-            format='%(levelname)s: %(message)s',
-            level=logging.INFO
-        )
-
-    cookiecutter(args.input_dir, args.checkout, args.no_input)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -15,12 +15,11 @@
 readme = open('README.rst').read()
 history = open('HISTORY.rst').read().replace('.. :changelog:', '')
 
-requirements = ['binaryornot>=0.2.0', 'jinja2>=2.4', 'PyYAML>=3.10']
+requirements = ['binaryornot>=0.2.0', 'jinja2>=2.4', 'PyYAML>=3.10', 'click<4.0']
 test_requirements = []
 
 # Add Python 2.6-specific dependencies
 if sys.version_info[:2] < (2, 7):
-    requirements.append('argparse')
     requirements.append('ordereddict')
     requirements.append('simplejson')
     test_requirements.append('unittest2')
@@ -47,7 +46,7 @@
     package_dir={'cookiecutter': 'cookiecutter'},
     entry_points={
         'console_scripts': [
-            'cookiecutter = cookiecutter.main:main',
+            'cookiecutter = cookiecutter.cli:main',
         ]
     },
     include_package_data=True,


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,25 @@
+import os
+
+from click.testing import CliRunner
+
+from cookiecutter.cli import main
+
+runner = CliRunner()
+
+
+def test_cli_version():
+    result = runner.invoke(main, ['-V'])
+    assert result.exit_code == 0
+    assert result.output.startswith('Cookiecutter')
+
+
+def test_cli():
+    result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input'])
+    assert result.exit_code == 0
+    assert os.path.isdir('fake-project')
+
+
+def test_cli_verbose():
+    result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input', '-v'])
+    assert result.exit_code == 0
+    assert os.path.isdir('fake-project')
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -100,44 +100,31 @@ def tearDown(self):
             utils.rmtree('fake-project-input-extra')
 
 
-class TestArgParsing(unittest.TestCase):
-
-    def test_parse_cookiecutter_args(self):
-        args = main.parse_cookiecutter_args(['project/'])
-        self.assertEqual(args.input_dir, 'project/')
-        self.assertEqual(args.checkout, None)
-
-    def test_parse_cookiecutter_args_with_branch(self):
-        args = main.parse_cookiecutter_args(['project/', '--checkout', 'develop'])
-        self.assertEqual(args.input_dir, 'project/')
-        self.assertEqual(args.checkout, 'develop')
-
-
 class TestAbbreviationExpansion(unittest.TestCase):
 
     def test_abbreviation_expansion(self):
-        input_dir = main.expand_abbreviations('foo', {'abbreviations': {'foo': 'bar'}})
-        self.assertEqual(input_dir, 'bar')
+        template = main.expand_abbreviations('foo', {'abbreviations': {'foo': 'bar'}})
+        self.assertEqual(template, 'bar')
 
     def test_abbreviation_expansion_not_an_abbreviation(self):
-        input_dir = main.expand_abbreviations('baz', {'abbreviations': {'foo': 'bar'}})
-        self.assertEqual(input_dir, 'baz')
+        template = main.expand_abbreviations('baz', {'abbreviations': {'foo': 'bar'}})
+        self.assertEqual(template, 'baz')
 
     def test_abbreviation_expansion_prefix(self):
-        input_dir = main.expand_abbreviations('xx:a', {'abbreviations': {'xx': '<{0}>'}})
-        self.assertEqual(input_dir, '<a>')
+        template = main.expand_abbreviations('xx:a', {'abbreviations': {'xx': '<{0}>'}})
+        self.assertEqual(template, '<a>')
 
     def test_abbreviation_expansion_builtin(self):
-        input_dir = main.expand_abbreviations('gh:a', {})
-        self.assertEqual(input_dir, 'https://github.com/a.git')
+        template = main.expand_abbreviations('gh:a', {})
+        self.assertEqual(template, 'https://github.com/a.git')
 
     def test_abbreviation_expansion_override_builtin(self):
-        input_dir = main.expand_abbreviations('gh:a', {'abbreviations': {'gh': '<{0}>'}})
-        self.assertEqual(input_dir, '<a>')
+        template = main.expand_abbreviations('gh:a', {'abbreviations': {'gh': '<{0}>'}})
+        self.assertEqual(template, '<a>')
 
     def test_abbreviation_expansion_prefix_ignores_suffix(self):
-        input_dir = main.expand_abbreviations('xx:a', {'abbreviations': {'xx': '<>'}})
-        self.assertEqual(input_dir, '<>')
+        template = main.expand_abbreviations('xx:a', {'abbreviations': {'xx': '<>'}})
+        self.assertEqual(template, '<>')
 
     def test_abbreviation_expansion_prefix_not_0_in_braces(self):
         self.assertRaises(