Repository: audreyr/cookiecutter
Pull request #471 (2015-07-09 20:55:01)

[Issue 470] Error out if project output directory exists.
Fix issue #470 by erroring out if the project output directory
exists. This will prevent cookiecutter from writing into the existing directories like it does now.

codecov-io: ## [Current coverage][1] is `95.42%`
> Merging **#471** into **master** will change coverage by **-0.13%** by [`ea02405`][3]


#### Coverage Diff
```diff
@@            master    #471   diff @@
======================================
  Files           12      12       
  Stmts          449     459    +10
  Branches         0       0       
  Methods          0       0       
======================================
+ Hit            429     438     +9
  Partial          0       0       
- Missed          20      21     +1
```



-----
### [Uncovered Suggestions][2]

1. `+0.88%` via [cookiecutter/vcs.py#59...62](https://codecov.io/github/audreyr/cookiecutter/cookiecutter/vcs.py?ref=ea024051076159e3cd4df096443aa8c00918c428#59) 
1. `+0.66%` via [...okiecutter/compat.py#78...80](https://codecov.io/github/audreyr/cookiecutter/cookiecutter/compat.py?ref=ea024051076159e3cd4df096443aa8c00918c428#78) 
1. `+0.66%` via [...okiecutter/compat.py#69...71](https://codecov.io/github/audreyr/cookiecutter/cookiecutter/compat.py?ref=ea024051076159e3cd4df096443aa8c00918c428#69) 
1. *[See 7 more...][2]*

[1]: https://codecov.io/github/audreyr/cookiecutter?ref=ea024051076159e3cd4df096443aa8c00918c428
[2]: https://codecov.io/github/audreyr/cookiecutter/features/suggestions?ref=ea024051076159e3cd4df096443aa8c00918c428
[3]: https://github.com/audreyr/cookiecutter/commit/ea024051076159e3cd4df096443aa8c00918c428

> Powered by [Codecov](https://codecov.io)
lgp171188: @hackebrot Made the suggested changes to ``cookiecutter/generate.py``. Please review and let me know if everything is fine.
lgp171188: @hackebrot I have omitted the ``scope`` argument for the ``fixture`` decorator since since ``function`` is the default 

Patch:
diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py
index 6702b61..8b2bda7 100755
--- a/cookiecutter/cli.py
+++ b/cookiecutter/cli.py
@@ -18,6 +18,7 @@ import click
 
 from cookiecutter import __version__
 from cookiecutter.main import cookiecutter
+from cookiecutter.exceptions import OutputDirExistsException
 
 logger = logging.getLogger(__name__)
 
@@ -67,4 +68,8 @@ def main(template, no_input, checkout, verbose):
             level=logging.INFO
         )
 
-    cookiecutter(template, checkout, no_input)
+    try:
+        cookiecutter(template, checkout, no_input)
+    except OutputDirExistsException as e:
+        click.echo(e)
+        sys.exit(1)
diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py
index fe7cf59..c1d626c 100755
--- a/cookiecutter/exceptions.py
+++ b/cookiecutter/exceptions.py
@@ -68,3 +68,9 @@ class ContextDecodingException(CookiecutterException):
     """
     Raised when a project's JSON context file can not be decoded.
     """
+
+
+class OutputDirExistsException(CookiecutterException):
+    """
+    Raised when the output directory of the project exists already.
+    """
diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py
index f21c544..8bc63a3 100755
--- a/cookiecutter/generate.py
+++ b/cookiecutter/generate.py
@@ -21,7 +21,11 @@ from jinja2.environment import Environment
 from jinja2.exceptions import TemplateSyntaxError
 from binaryornot.check import is_binary
 
-from .exceptions import NonTemplatedInputDirException, ContextDecodingException
+from .exceptions import (
+    NonTemplatedInputDirException,
+    ContextDecodingException,
+    OutputDirExistsException
+)
 from .find import find_template
 from .utils import make_sure_path_exists, work_in
 from .hooks import run_hook
@@ -148,7 +152,7 @@ def generate_file(project_dir, infile, context, env):
     shutil.copymode(infile, outfile)
 
 
-def render_and_create_dir(dirname, context, output_dir):
+def render_and_create_dir(dirname, context, output_dir, fail_if_exists=False):
     """
     Renders the name of a directory, creates the directory, and
     returns its path.
@@ -163,6 +167,12 @@ def render_and_create_dir(dirname, context, output_dir):
     dir_to_create = os.path.normpath(
         os.path.join(output_dir, rendered_dirname)
     )
+
+    if fail_if_exists:
+        if os.path.exists(dir_to_create):
+            msg = 'Error: "{}" directory already exists'.format(dir_to_create)
+            raise OutputDirExistsException(msg)
+
     make_sure_path_exists(dir_to_create)
     return dir_to_create
 
@@ -192,7 +202,10 @@ def generate_files(repo_dir, context=None, output_dir='.'):
 
     unrendered_dir = os.path.split(template_dir)[1]
     ensure_dir_is_templated(unrendered_dir)
-    project_dir = render_and_create_dir(unrendered_dir, context, output_dir)
+    project_dir = render_and_create_dir(unrendered_dir,
+                                        context,
+                                        output_dir,
+                                        fail_if_exists=True)
 
     # We want the Jinja path and the OS paths to match. Consequently, we'll:
     #   + CD to the template folder


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index d202361..d45e6ec 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,24 +1,56 @@
 import os
+import pytest
 
 from click.testing import CliRunner
 
 from cookiecutter.cli import main
+from cookiecutter import utils
 
 runner = CliRunner()
 
 
+@pytest.fixture
+def remove_fake_project_dir(request):
+    """
+    Remove the fake project directory created during the tests.
+    """
+    def fin_remove_fake_project_dir():
+        if os.path.isdir('fake-project'):
+            utils.rmtree('fake-project')
+    request.addfinalizer(fin_remove_fake_project_dir)
+
+
+@pytest.fixture
+def make_fake_project_dir(request):
+    """
+    Create the fake project directory created during the tests.
+    """
+    if not os.path.isdir('fake-project'):
+        os.makedirs('fake-project')
+
+
 def test_cli_version():
     result = runner.invoke(main, ['-V'])
     assert result.exit_code == 0
     assert result.output.startswith('Cookiecutter')
 
 
+@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
+def test_cli_error_on_existing_output_directory():
+    result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input'])
+    assert result.exit_code != 0
+    expected_error_msg = 'Error: "fake-project" directory already exists\n'
+    assert result.output == expected_error_msg
+
+
+@pytest.mark.usefixtures('remove_fake_project_dir')
 def test_cli():
     result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input'])
     assert result.exit_code == 0
     assert os.path.isdir('fake-project')
 
 
+@pytest.mark.usefixtures('remove_fake_project_dir')
 def test_cli_verbose():
     result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input', '-v'])
     assert result.exit_code == 0
diff --git a/tests/test_output_folder.py b/tests/test_output_folder.py
index 06b2ffe..671ef54 100644
--- a/tests/test_output_folder.py
+++ b/tests/test_output_folder.py
@@ -15,6 +15,7 @@ import pytest
 
 from cookiecutter import generate
 from cookiecutter import utils
+from cookiecutter import exceptions
 
 
 @pytest.fixture(scope='function')
@@ -50,3 +51,19 @@ It is 2014."""
 
     assert os.path.isdir('output_folder/im_a.dir')
     assert os.path.isfile('output_folder/im_a.dir/im_a.file.py')
+
+
+@pytest.mark.usefixtures('clean_system', 'remove_output_folder')
+def test_exception_when_output_folder_exists():
+    context = generate.generate_context(
+        context_file='tests/test-output-folder/cookiecutter.json'
+    )
+    output_folder = context['cookiecutter']['test_name']
+
+    if not os.path.exists(output_folder):
+        os.makedirs(output_folder)
+    with pytest.raises(exceptions.OutputDirExistsException):
+        generate.generate_files(
+            context=context,
+            repo_dir='tests/test-output-folder'
+        )