Repository: cookiecutter/cookiecutter

Issue:
output_dir for python API invocation + parametrize output_dir tests
Need this for a cookiecutter project that's tested using [python api](http://cookiecutter.readthedocs.org/en/latest/advanced_usage.html#calling-cookiecutter-functions-from-python).

Also parametrize output_dir tests and add testcase for cookiecutter python object.



Discussion:
@pydanny @audreyr : If the new tests look good, this will unblock me on one of my cookiecutters (https://github.com/tony/cookiecutter-flask-pythonic/commit/ff8a74ebe18e076a7da2ae9e047d690f70efcc58). I'm trying to get tests and TDD ramped up for boilerplates. When I get it smoothed out - maybe I can write a section on it

I can now avoid import problems when rerunning tests while watching files.

Dupe of #90 ?

@cabello It appears so.

@davedash : It appears I have duplicated your issue.  I'd prefer to see you get credit, since you were first at it.

The only thing that differs is the tests.  I do like the extra assurances parametrizing gives in case anything is factored.  If those look ok we can always add it afterwards.

Any updates on this issue by chance? I could really use it for a project and I'd prefer to not have to duplicate functionality to accomplish. Thanks!

Oh sorry I see my name is mentioned here - um, I think someone actively using cookiecutter should take this over - hopefully @tony , but honestly @jefftriplett if you're blocked I think it's fine to duplicate effort.

We're planning to do a release of Cookiecutter tomorrow. We'll review this before then.

Thank you for the quick response. I completely understand if it needs more time. Just thought I'd ask to see since it seemed like this PR covers a usecase I ran into tonight. Thanks everyone!


Resolving patch:
diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py
--- a/cookiecutter/cli.py
+++ b/cookiecutter/cli.py
@@ -55,7 +55,12 @@ def version_msg():
     u'-f', u'--overwrite-if-exists', is_flag=True,
     help=u'Overwrite the contents of the output directory if it already exists'
 )
-def main(template, no_input, checkout, verbose, replay, overwrite_if_exists):
+@click.option(
+    u'-o', u'--output-dir', default='.', type=click.Path(),
+    help=u'Where to output the generated project dir into'
+)
+def main(template, no_input, checkout, verbose, replay, overwrite_if_exists,
+         output_dir):
     """Create a project from a Cookiecutter project template (TEMPLATE)."""
     if verbose:
         logging.basicConfig(
@@ -70,8 +75,12 @@ def main(template, no_input, checkout, verbose, replay, overwrite_if_exists):
         )
 
     try:
-        cookiecutter(template, checkout, no_input, replay=replay,
-                     overwrite_if_exists=overwrite_if_exists)
+        cookiecutter(
+            template, checkout, no_input,
+            replay=replay,
+            overwrite_if_exists=overwrite_if_exists,
+            output_dir=output_dir
+        )
     except (OutputDirExistsException, InvalidModeException) as e:
         click.echo(e)
         sys.exit(1)
diff --git a/cookiecutter/main.py b/cookiecutter/main.py
--- a/cookiecutter/main.py
+++ b/cookiecutter/main.py
@@ -71,7 +71,7 @@ def expand_abbreviations(template, config_dict):
 
 def cookiecutter(
         template, checkout=None, no_input=False, extra_context=None,
-        replay=False, overwrite_if_exists=False):
+        replay=False, overwrite_if_exists=False, output_dir='.'):
     """
     API equivalent to using Cookiecutter at the command line.
 
@@ -83,6 +83,7 @@ def cookiecutter(
         and user configuration.
     :param: overwrite_if_exists: Overwrite the contents of output directory
         if it exists
+    :param output_dir: Where to output the generated project dir into.
     """
     if replay and ((no_input is not False) or (extra_context is not None)):
         err_msg = (
@@ -133,5 +134,6 @@ def cookiecutter(
     generate_files(
         repo_dir=repo_dir,
         context=context,
-        overwrite_if_exists=overwrite_if_exists
+        overwrite_if_exists=overwrite_if_exists,
+        output_dir=output_dir
     )


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -79,7 +79,8 @@ def test_cli_replay(mocker):
         None,
         False,
         replay=True,
-        overwrite_if_exists=False
+        overwrite_if_exists=False,
+        output_dir='.'
     )
 
 
@@ -112,7 +113,8 @@ def test_cli_exit_on_noinput_and_replay(mocker):
         None,
         True,
         replay=True,
-        overwrite_if_exists=False
+        overwrite_if_exists=False,
+        output_dir='.'
     )
 
 
@@ -144,7 +146,8 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay(
         None,
         False,
         replay=True,
-        overwrite_if_exists=True
+        overwrite_if_exists=True,
+        output_dir='.'
     )
 
 
@@ -167,3 +170,36 @@ def test_cli_overwrite_if_exists_when_output_dir_exists(overwrite_cli_flag):
 
     assert result.exit_code == 0
     assert os.path.isdir('fake-project')
+
+
+@pytest.fixture(params=['-o', '--output-dir'])
+def output_dir_flag(request):
+    return request.param
+
+
+@pytest.fixture
+def output_dir(tmpdir):
+    return str(tmpdir.mkdir('output'))
+
+
+def test_cli_output_dir(mocker, output_dir_flag, output_dir):
+    mock_cookiecutter = mocker.patch(
+        'cookiecutter.cli.cookiecutter'
+    )
+
+    template_path = 'tests/fake-repo-pre/'
+    result = runner.invoke(main, [
+        template_path,
+        output_dir_flag,
+        output_dir
+    ])
+
+    assert result.exit_code == 0
+    mock_cookiecutter.assert_called_once_with(
+        template_path,
+        None,
+        False,
+        replay=False,
+        overwrite_if_exists=False,
+        output_dir=output_dir
+    )
diff --git a/tests/test_specify_output_dir.py b/tests/test_specify_output_dir.py
new file mode 100644
--- /dev/null
+++ b/tests/test_specify_output_dir.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from cookiecutter import main
+
+
+@pytest.fixture
+def context():
+    """Fixture to return a valid context as known from a cookiecutter.json."""
+    return {
+        u'cookiecutter': {
+            u'email': u'raphael@hackebrot.de',
+            u'full_name': u'Raphael Pierzina',
+            u'github_username': u'hackebrot',
+            u'version': u'0.1.0',
+        }
+    }
+
+
+@pytest.fixture
+def output_dir(tmpdir):
+    return str(tmpdir.mkdir('output'))
+
+
+@pytest.fixture
+def template(tmpdir):
+    return str(tmpdir.mkdir('template'))
+
+
+@pytest.fixture(autouse=True)
+def mock_gen_context(mocker, context):
+    mocker.patch('cookiecutter.main.generate_context', return_value=context)
+
+
+@pytest.fixture(autouse=True)
+def mock_prompt(mocker):
+    mocker.patch('cookiecutter.main.prompt_for_config')
+
+
+@pytest.fixture(autouse=True)
+def mock_replay(mocker):
+    mocker.patch('cookiecutter.main.dump')
+
+
+def test_api_invocation(mocker, template, output_dir, context):
+    mock_gen_files = mocker.patch('cookiecutter.main.generate_files')
+
+    main.cookiecutter(template, output_dir=output_dir)
+
+    mock_gen_files.assert_called_once_with(
+        repo_dir=template,
+        context=context,
+        overwrite_if_exists=False,
+        output_dir=output_dir
+    )
+
+
+def test_default_output_dir(mocker, template, context):
+    mock_gen_files = mocker.patch('cookiecutter.main.generate_files')
+
+    main.cookiecutter(template)
+
+    mock_gen_files.assert_called_once_with(
+        repo_dir=template,
+        context=context,
+        overwrite_if_exists=False,
+        output_dir='.'
+    )