Repository: audreyr/cookiecutter
Pull request #782 (2016-07-15 21:06:48)

Ensure cookiecutter repositories have json context

codecov-io: ## [Current coverage](https://codecov.io/gh/audreyr/cookiecutter/pull/782?src=pr) is 100% (diff: 100%)
> Merging [#782](https://codecov.io/gh/audreyr/cookiecutter/pull/782?src=pr) into [master](https://codecov.io/gh/audreyr/cookiecutter/branch/master?src=pr) will not change coverage


```diff
@@           master   #782   diff @@
====================================
  Files          14     14          
  Lines         599    595     -4   
  Methods         0      0          
  Messages        0      0          
  Branches        0      0          
====================================
- Hits          599    595     -4   
  Misses          0      0          
  Partials        0      0          
```

![Sunburst](https://codecov.io/gh/audreyr/cookiecutter/pull/782/graphs/sunburst.svg?src=pr&size=150)

> Powered by [Codecov](https://codecov.io?src=pr). Last update [5db20b2...bbbfa77](https://codecov.io/gh/audreyr/cookiecutter/compare/5db20b224ec4e36fbd8dfc74ea6a97488596a7d9...bbbfa77441dbc8fb9fcd850a6bfd5e08c8f34af4?src=pr)

Patch:
diff --git a/cookiecutter/repository.py b/cookiecutter/repository.py
index 1ec9e37..e7ec4fc 100644
--- a/cookiecutter/repository.py
+++ b/cookiecutter/repository.py
@@ -41,6 +41,20 @@ def expand_abbreviations(template, abbreviations):
     return template
 
 
+def repository_has_cookiecutter_json(repo_directory):
+    """Determines if `repo_directory` contains a `cookiecutter.json` file.
+
+    :param repo_directory: The candidate repository directory.
+    :return: True if the `repo_directory` is valid, else False.
+    """
+    repo_directory_exists = os.path.isdir(repo_directory)
+
+    repo_config_exists = os.path.isfile(
+        os.path.join(repo_directory, 'cookiecutter.json')
+    )
+    return repo_directory_exists and repo_config_exists
+
+
 def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
                        no_input):
     """
@@ -74,8 +88,10 @@ def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
         # cookiecutters_dir
         repo_dir = template
 
-    if not os.path.isdir(repo_dir):
-        raise RepositoryNotFound(
-            'The repository {0} could not be located.'.format(template)
-        )
-    return repo_dir
+    if repository_has_cookiecutter_json(repo_dir):
+        return repo_dir
+
+    raise RepositoryNotFound(
+        'The repository {} could not be located or does not contain '
+        'a "cookiecutter.json" file.'.format(repo_dir)
+    )


Test patch:
diff --git a/tests/fake-repo/cookiecutter.json b/tests/fake-repo/cookiecutter.json
new file mode 100644
index 0000000..e69de29
diff --git a/tests/repository/test_determine_repo_dir_clones_repo.py b/tests/repository/test_determine_repo_dir_clones_repo.py
index 58b782a..c9b6afe 100644
--- a/tests/repository/test_determine_repo_dir_clones_repo.py
+++ b/tests/repository/test_determine_repo_dir_clones_repo.py
@@ -3,7 +3,7 @@ import os
 
 import pytest
 
-from cookiecutter.repository import determine_repo_dir
+from cookiecutter import repository, exceptions
 
 
 @pytest.fixture
@@ -15,10 +15,10 @@ def template_url():
     return 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git'
 
 
-def test_determine_repository_url_should_clone(
+def test_repository_url_should_clone(
         mocker, template_url, user_config_data):
-    """`clone()` should be called with correct args when `cookiecutter()` is
-    called.
+    """`clone()` should be called with correct args when
+    `determine_repo_dir()` is passed a repository template url.
     """
 
     mock_clone = mocker.patch(
@@ -27,7 +27,7 @@ def test_determine_repository_url_should_clone(
         autospec=True
     )
 
-    project_dir = determine_repo_dir(
+    project_dir = repository.determine_repo_dir(
         template_url,
         abbreviations={},
         clone_to_dir=user_config_data['cookiecutters_dir'],
@@ -43,3 +43,26 @@ def test_determine_repository_url_should_clone(
     )
 
     assert os.path.isdir(project_dir)
+    assert 'tests/fake-repo-tmpl' == project_dir
+
+
+def test_repository_url_with_no_context_file(mocker, template_url):
+    mocker.patch(
+        'cookiecutter.repository.clone',
+        return_value='tests/fake-repo-bad',
+        autospec=True
+    )
+
+    with pytest.raises(exceptions.RepositoryNotFound) as err:
+        repository.determine_repo_dir(
+            template_url,
+            abbreviations={},
+            clone_to_dir=None,
+            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.'
+    )
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 8396660..b3af470 100644
--- a/tests/repository/test_determine_repository_should_use_local_repo.py
+++ b/tests/repository/test_determine_repository_should_use_local_repo.py
@@ -1,9 +1,12 @@
 # -*- coding: utf-8 -*-
-from cookiecutter.repository import determine_repo_dir
+from cookiecutter import repository, exceptions
 
+import pytest
 
-def test_determine_repository_should_use_local_repo():
-    project_dir = determine_repo_dir(
+
+def test_finds_local_repo():
+    """A valid local repository should be returned."""
+    project_dir = repository.determine_repo_dir(
         'tests/fake-repo',
         abbreviations={},
         clone_to_dir=None,
@@ -12,3 +15,41 @@ def test_determine_repository_should_use_local_repo():
     )
 
     assert 'tests/fake-repo' == project_dir
+
+
+def test_local_repo_with_no_context_raises():
+    """A local repository without a cookiecutter.json should raise a
+    `RepositoryNotFound` exception.
+    """
+    with pytest.raises(exceptions.RepositoryNotFound) as err:
+        repository.determine_repo_dir(
+            'tests/fake-repo-bad',
+            abbreviations={},
+            clone_to_dir=None,
+            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.'
+    )
+
+
+def test_local_repo_typo():
+    """An unknown local repository should raise a `RepositoryNotFound`
+    exception.
+    """
+    with pytest.raises(exceptions.RepositoryNotFound) as err:
+        repository.determine_repo_dir(
+            'tests/unknown-repo',
+            abbreviations={},
+            clone_to_dir=None,
+            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.'
+    )
diff --git a/tests/repository/test_repository_has_cookiecutter_json.py b/tests/repository/test_repository_has_cookiecutter_json.py
new file mode 100644
index 0000000..5d53889
--- /dev/null
+++ b/tests/repository/test_repository_has_cookiecutter_json.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from cookiecutter.repository import repository_has_cookiecutter_json
+
+import pytest
+
+
+def test_valid_repository():
+    assert repository_has_cookiecutter_json('tests/fake-repo')
+
+
+@pytest.fixture(params=[
+    'tests/fake-repo-bad',
+    'tests/unknown-repo',
+])
+def invalid_repository(request):
+    return request.param
+
+
+def test_invalid_repository(invalid_repository):
+    assert not repository_has_cookiecutter_json(invalid_repository)
diff --git a/tests/test_specify_output_dir.py b/tests/test_specify_output_dir.py
index b62a6fa..f20ce98 100644
--- a/tests/test_specify_output_dir.py
+++ b/tests/test_specify_output_dir.py
@@ -25,7 +25,9 @@ def output_dir(tmpdir):
 
 @pytest.fixture
 def template(tmpdir):
-    return str(tmpdir.mkdir('template'))
+    template_dir = tmpdir.mkdir('template')
+    template_dir.join('cookiecutter.json').ensure(file=True)
+    return str(template_dir)
 
 
 @pytest.fixture(autouse=True)