Repository: audreyr/cookiecutter (python)

Issue:
.cookiecutterrc and cookiecutters_dir not working as I expected
Hi,
Here's the setup:
+ I have a ~/.cookiecutterrc as talked about here: http://cookiecutter.readthedocs.org/en/latest/advanced_usage.html
+ I also have a cookiecutters_dir at ~/.cookiecutters/ with the subdirectory cookiecutter-pypackage/.

When I try to run "cookiecutter cookiecutter-pypackage/" in ~/Projects/, I get the following error

    Traceback (most recent call last):
      File "/opt/anaconda/bin/cookiecutter", line 9, in <module>
        load_entry_point('cookiecutter==0.9.0', 'console_scripts', 'cookiecutter')()
      File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 610, in __call__
        return self.main(*args, **kwargs)
      File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 590, in main
        rv = self.invoke(ctx)
      File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 782, in invoke
        return ctx.invoke(self.callback, **ctx.params)
      File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 416, in invoke
        return callback(*args, **kwargs)
      File "/Users/alexp/Projects/cookiecutter/cookiecutter/cli.py", line 70, in main
        cookiecutter(template, checkout, no_input)
      File "/Users/alexp/Projects/cookiecutter/cookiecutter/main.py", line 95, in cookiecutter
        extra_context=extra_context,
      File "/Users/alexp/Projects/cookiecutter/cookiecutter/generate.py", line 43, in generate_context
        file_handle = open(context_file)
    IOError: [Errno 2] No such file or directory: u'cookiecutter-pypackage/cookiecutter.json'

This error shows up if I either do pip install or with the git repo locally.  Naturally, it makes a bit of sense.  There is no directory ~/Projects/cookiecutter-pypackage/.  

However, and perhaps I'm making a poor assumption about usage, it seems to me if I clone or otherwise create a cookiecutter and it's sitting in cookiecutters_dir, it'd be nice to just refer to it as I did above.  For my usage, if I create a cookiecutter, I don't particularly want it sitting around a Projects directory, especially if I have multiple project directories for personal and organizational purposes.

In order to do this, I added three lines to main.py in my fork (see below) and it seems to work.  I did it as an `elif` to try to preserve the possibility of a lack of a cookiecutters_dir.  I have not written a test for this and admittedly I don't really know how.  I will likely just use my fork with this modificationgoing forward but I wanted to let the developer crew know about this.  

Cheers.

    # cookiecutter.main
    ...
    # TODO: find a better way to tell if it's a repo URL
    if 'git@' in template or 'https://' in template:
        repo_dir = clone(
            repo_url=template,
            checkout=checkout,
            clone_to_dir=config_dict['cookiecutters_dir'],
            no_input=no_input
        )
    #### Added these three lines
    elif 'cookiecutters_dir' in config_dict:
        cookiecutters_dir = os.path.expanduser(config_dict['cookiecutters_dir'])
        repo_dir = os.path.join(cookiecutters_dir,template)
    else:
        # If it's a local repo, no need to clone or copy to your
        # cookiecutters_dir
        repo_dir = template

Pull request:
Use existing cookiecutters
Existing cookiecutters in `~/.cookiecutters` can now be referenced by directory name.

Fixes #385.

Patch:
diff --git a/README.rst b/README.rst
index 4061d7b..62ec24c 100644
--- a/README.rst
+++ b/README.rst
@@ -67,7 +67,7 @@ Did someone say features?
         # For the sake of brevity, repos on GitHub can just use the 'gh' prefix
         $ cookiecutter gh:audreyr/cookiecutter-pypackage
 
-* Can also use it at the command line with a local template:
+* Use it at the command line with a local template:
 
     .. code-block:: bash
 
@@ -95,7 +95,7 @@ Did someone say features?
 
 * 100% of templating is done with Jinja2. This includes file and directory names.
 
-* Simply define your template variables in a `cookiecutter.json` file. For example:
+* Simply define your template variables in a ``cookiecutter.json`` file. For example:
 
     .. code-block:: json
 
@@ -110,13 +110,13 @@ Did someone say features?
             "version": "0.1.1"
         }
 
-* Unless you suppress it with `--no-input`, you are prompted for input:
+* Unless you suppress it with ``--no-input``, you are prompted for input:
 
-  - Prompts are the keys in `cookiecutter.json`.
-  - Default responses are the values in `cookiecutter.json`.
+  - Prompts are the keys in ``cookiecutter.json``.
+  - Default responses are the values in ``cookiecutter.json``.
   - Prompts are shown in order.
 
-* Cross-platform support for `~/.cookiecutterrc` files:
+* Cross-platform support for ``~/.cookiecutterrc`` files:
 
     .. code-block:: yaml
 
@@ -127,7 +127,17 @@ Did someone say features?
         cookiecutters_dir: "~/.cookiecutters/"
 
 * Cookiecutters (cloned Cookiecutter project templates) are put into
-  `~/.cookiecutters/` by default, or cookiecutters_dir if specified.
+  ``~/.cookiecutters/`` by default, or cookiecutters_dir if specified.
+
+* If you have already cloned a cookiecutter into ``~/.cookiecutters/``, you
+  can reference it by directory name:
+
+    .. code-block:: bash
+
+        # Clone cookiecutter-pypackage
+        $ cookiecutter gh:audreyr/cookiecutter-pypackage
+        # Now you can use the already cloned cookiecutter by name
+        $ cookiecutter cookiecutter-pypackage
 
 * You can use local cookiecutters, or remote cookiecutters directly from Git
   repos or from Mercurial repos on Bitbucket.
diff --git a/cookiecutter/repository.py b/cookiecutter/repository.py
index e7ec4fc..7a9e494 100644
--- a/cookiecutter/repository.py
+++ b/cookiecutter/repository.py
@@ -77,21 +77,27 @@ def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
     template = expand_abbreviations(template, abbreviations)
 
     if is_repo_url(template):
-        repo_dir = clone(
+        cloned_repo = clone(
             repo_url=template,
             checkout=checkout,
             clone_to_dir=clone_to_dir,
             no_input=no_input,
         )
+        repository_candidates = [cloned_repo]
     else:
-        # If it's a local repo, no need to clone or copy to your
-        # cookiecutters_dir
-        repo_dir = template
+        repository_candidates = [
+            template,
+            os.path.join(clone_to_dir, template)
+        ]
 
-    if repository_has_cookiecutter_json(repo_dir):
-        return repo_dir
+    for repo_candidate in repository_candidates:
+        if repository_has_cookiecutter_json(repo_candidate):
+            return repo_candidate
 
     raise RepositoryNotFound(
-        'The repository {} could not be located or does not contain '
-        'a "cookiecutter.json" file.'.format(repo_dir)
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template,
+            '\n'.join(repository_candidates)
+        )
     )


Test patch:
diff --git a/tests/repository/test_determine_repo_dir_clones_repo.py b/tests/repository/test_determine_repo_dir_clones_repo.py
index c9b6afe..cff8dd4 100644
--- a/tests/repository/test_determine_repo_dir_clones_repo.py
+++ b/tests/repository/test_determine_repo_dir_clones_repo.py
@@ -46,7 +46,8 @@ def test_repository_url_should_clone(
     assert 'tests/fake-repo-tmpl' == project_dir
 
 
-def test_repository_url_with_no_context_file(mocker, template_url):
+def test_repository_url_with_no_context_file(
+        mocker, template_url, user_config_data):
     mocker.patch(
         'cookiecutter.repository.clone',
         return_value='tests/fake-repo-bad',
@@ -63,6 +64,9 @@ def test_repository_url_with_no_context_file(mocker, template_url):
         )
 
     assert str(err.value) == (
-        'The repository tests/fake-repo-bad could not be located or does not '
-        'contain a "cookiecutter.json" file.'
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template_url,
+            'tests/fake-repo-bad',
+        )
     )
diff --git a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py
new file mode 100644
index 0000000..7b274a3
--- /dev/null
+++ b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+import io
+import os
+import pytest
+
+from cookiecutter import repository
+
+
+@pytest.fixture
+def template():
+    return 'cookiecutter-pytest-plugin'
+
+
+@pytest.fixture
+def cloned_cookiecutter_path(user_config_data, template):
+    cookiecutters_dir = user_config_data['cookiecutters_dir']
+
+    cloned_template_path = os.path.join(cookiecutters_dir, template)
+    os.mkdir(cloned_template_path)
+
+    io.open(os.path.join(cloned_template_path, 'cookiecutter.json'), 'w')
+
+    return cloned_template_path
+
+
+def test_should_find_existing_cookiecutter(
+        template, user_config_data, cloned_cookiecutter_path):
+    project_dir = repository.determine_repo_dir(
+        template,
+        abbreviations={},
+        clone_to_dir=user_config_data['cookiecutters_dir'],
+        checkout=None,
+        no_input=True,
+    )
+
+    assert cloned_cookiecutter_path == project_dir
diff --git a/tests/repository/test_determine_repository_should_use_local_repo.py b/tests/repository/test_determine_repository_should_use_local_repo.py
index b3af470..b2ca4f1 100644
--- a/tests/repository/test_determine_repository_should_use_local_repo.py
+++ b/tests/repository/test_determine_repository_should_use_local_repo.py
@@ -1,15 +1,16 @@
 # -*- coding: utf-8 -*-
+import os
 from cookiecutter import repository, exceptions
 
 import pytest
 
 
-def test_finds_local_repo():
+def test_finds_local_repo(tmpdir):
     """A valid local repository should be returned."""
     project_dir = repository.determine_repo_dir(
         'tests/fake-repo',
         abbreviations={},
-        clone_to_dir=None,
+        clone_to_dir=str(tmpdir),
         checkout=None,
         no_input=True
     )
@@ -17,39 +18,53 @@ def test_finds_local_repo():
     assert 'tests/fake-repo' == project_dir
 
 
-def test_local_repo_with_no_context_raises():
+def test_local_repo_with_no_context_raises(tmpdir):
     """A local repository without a cookiecutter.json should raise a
     `RepositoryNotFound` exception.
     """
+    template_path = os.path.join('tests', 'fake-repo-bad')
     with pytest.raises(exceptions.RepositoryNotFound) as err:
         repository.determine_repo_dir(
-            'tests/fake-repo-bad',
+            template_path,
             abbreviations={},
-            clone_to_dir=None,
+            clone_to_dir=str(tmpdir),
             checkout=None,
             no_input=True
         )
 
     assert str(err.value) == (
-        'The repository tests/fake-repo-bad could not be located or does not '
-        'contain a "cookiecutter.json" file.'
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template_path,
+            '\n'.join([
+                template_path,
+                str(tmpdir / 'tests/fake-repo-bad')
+            ]),
+        )
     )
 
 
-def test_local_repo_typo():
+def test_local_repo_typo(tmpdir):
     """An unknown local repository should raise a `RepositoryNotFound`
     exception.
     """
+    template_path = os.path.join('tests', 'unknown-repo')
     with pytest.raises(exceptions.RepositoryNotFound) as err:
         repository.determine_repo_dir(
-            'tests/unknown-repo',
+            template_path,
             abbreviations={},
-            clone_to_dir=None,
+            clone_to_dir=str(tmpdir),
             checkout=None,
             no_input=True
         )
 
     assert str(err.value) == (
-        'The repository tests/unknown-repo could not be located or does not '
-        'contain a "cookiecutter.json" file.'
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template_path,
+            '\n'.join([
+                template_path,
+                str(tmpdir / 'tests/unknown-repo')
+            ]),
+        )
     )