Repository: audreyr/cookiecutter
Pull request #425 (2015-03-23 00:05:26)

Hackebrot merge copy without render
Continue the work on #132 and #184.

Resolving the conflicts was pointless, so I manually merged in the changes. Credit goes to @osantana and @LucianU :bow:

I converted the former test to py.test, fixed flake8 and changed string literals to single quotes.

According to the previous PRs we should get failures on appveyor. I'll look into them in the next days. Tests are fine though on Ubuntu and the coverage does not drop. (We need codecov comments :cry: #416)

cc: @audreyr @pydanny 


Patch:
diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py
index 092008b..f21c544 100755
--- a/cookiecutter/generate.py
+++ b/cookiecutter/generate.py
@@ -9,6 +9,7 @@ Functions for generating a project from a project template.
 """
 from __future__ import unicode_literals
 from collections import OrderedDict
+import fnmatch
 import io
 import json
 import logging
@@ -26,6 +27,25 @@ from .utils import make_sure_path_exists, work_in
 from .hooks import run_hook
 
 
+def copy_without_render(path, context):
+    """
+    Returns True if `path` matches some pattern in the
+    `_copy_without_render` context setting.
+
+    :param path: A file-system path referring to a file or dir that
+        should be rendered or just copied.
+    :param context: cookiecutter context.
+    """
+    try:
+        for dont_render in context['cookiecutter']['_copy_without_render']:
+            if fnmatch.fnmatch(path, dont_render):
+                return True
+    except KeyError:
+        return False
+
+    return False
+
+
 def generate_context(context_file='cookiecutter.json', default_context=None,
                      extra_context=None):
     """
@@ -193,13 +213,51 @@ def generate_files(repo_dir, context=None, output_dir='.'):
         env.loader = FileSystemLoader('.')
 
         for root, dirs, files in os.walk('.'):
+            # We must separate the two types of dirs into different lists.
+            # The reason is that we don't want ``os.walk`` to go through the
+            # unrendered directories, since they will just be copied.
+            copy_dirs = []
+            render_dirs = []
+
+            for d in dirs:
+                d_ = os.path.normpath(os.path.join(root, d))
+                # We check the full path, because that's how it can be
+                # specified in the ``_copy_without_render`` setting, but
+                # we store just the dir name
+                if copy_without_render(d_, context):
+                    copy_dirs.append(d)
+                else:
+                    render_dirs.append(d)
+
+            for copy_dir in copy_dirs:
+                indir = os.path.normpath(os.path.join(root, copy_dir))
+                outdir = os.path.normpath(os.path.join(project_dir, indir))
+                logging.debug(
+                    'Copying dir {0} to {1} without rendering'
+                    ''.format(indir, outdir)
+                )
+                shutil.copytree(indir, outdir)
+
+            # We mutate ``dirs``, because we only want to go through these dirs
+            # recursively
+            dirs[:] = render_dirs
             for d in dirs:
-                unrendered_dir = os.path.join(project_dir,
-                                              os.path.join(root, d))
+                unrendered_dir = os.path.join(project_dir, root, d)
                 render_and_create_dir(unrendered_dir, context, output_dir)
 
             for f in files:
-                infile = os.path.join(root, f)
+                infile = os.path.normpath(os.path.join(root, f))
+                if copy_without_render(infile, context):
+                    outfile_tmpl = Template(infile)
+                    outfile_rendered = outfile_tmpl.render(**context)
+                    outfile = os.path.join(project_dir, outfile_rendered)
+                    logging.debug(
+                        'Copying file {0} to {1} without rendering'
+                        ''.format(infile, outfile)
+                    )
+                    shutil.copyfile(infile, outfile)
+                    shutil.copymode(infile, outfile)
+                    continue
                 logging.debug('f is {0}'.format(f))
                 generate_file(project_dir, infile, context, env)
 
diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py
index eafc23a..5e8bd67 100755
--- a/cookiecutter/prompt.py
+++ b/cookiecutter/prompt.py
@@ -26,6 +26,10 @@ def prompt_for_config(context, no_input=False):
     env = Environment()
 
     for key, raw in iteritems(context['cookiecutter']):
+        if key.startswith('_'):
+            cookiecutter_dict[key] = raw
+            continue
+
         raw = raw if is_string(raw) else str(raw)
         val = env.from_string(raw).render(cookiecutter=cookiecutter_dict)
 
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
index b4fe8fe..9dbabd2 100644
--- a/docs/troubleshooting.rst
+++ b/docs/troubleshooting.rst
@@ -27,6 +27,18 @@ Or this::
 
 See http://jinja.pocoo.org/docs/templates/#escaping for more info.
 
+You can also use the `_copy_without_render` key in your `cookiecutter.json`
+file, which accepts Unix shell-style wildcards::
+
+    {
+        "repo_name": "sample",
+        "_copy_without_render": [
+            "*.html",
+            "*not_rendered_dir",
+            "rendered_dir/not_rendered_file.ini"
+        ]
+    }
+
 Other common issues
 -------------------
 


Test patch:
diff --git a/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/README.rst b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/README.rst
new file mode 100644
index 0000000..2413548
--- /dev/null
+++ b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/README.rst
@@ -0,0 +1,5 @@
+============
+Fake Project
+============
+
+{{cookiecutter.render_test}}
diff --git a/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/README.txt b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/README.txt
new file mode 100644
index 0000000..2413548
--- /dev/null
+++ b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/README.txt
@@ -0,0 +1,5 @@
+============
+Fake Project
+============
+
+{{cookiecutter.render_test}}
diff --git a/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/rendered/not_rendered.yml b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/rendered/not_rendered.yml
new file mode 100644
index 0000000..a31cf75
--- /dev/null
+++ b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/rendered/not_rendered.yml
@@ -0,0 +1,2 @@
+---
+- name: {{cookiecutter.render_test}}
diff --git a/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst
new file mode 100644
index 0000000..2413548
--- /dev/null
+++ b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst
@@ -0,0 +1,5 @@
+============
+Fake Project
+============
+
+{{cookiecutter.render_test}}
diff --git a/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst
new file mode 100644
index 0000000..2413548
--- /dev/null
+++ b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst
@@ -0,0 +1,5 @@
+============
+Fake Project
+============
+
+{{cookiecutter.render_test}}
diff --git a/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt
new file mode 100644
index 0000000..2413548
--- /dev/null
+++ b/tests/test-generate-copy-without-render/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt
@@ -0,0 +1,5 @@
+============
+Fake Project
+============
+
+{{cookiecutter.render_test}}
diff --git a/tests/test_generate_copy_without_render.py b/tests/test_generate_copy_without_render.py
new file mode 100644
index 0000000..6bc0179
--- /dev/null
+++ b/tests/test_generate_copy_without_render.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+test_generate_copy_without_render
+---------------------------------
+"""
+
+from __future__ import unicode_literals
+import os
+import pytest
+
+from cookiecutter import generate
+from cookiecutter import utils
+
+
+@pytest.fixture(scope='function')
+def remove_test_dir(request):
+    """
+    Remove the folder that is created by the test.
+    """
+    def fin_remove_test_dir():
+        if os.path.exists('test_copy_without_render'):
+            utils.rmtree('test_copy_without_render')
+    request.addfinalizer(fin_remove_test_dir)
+
+
+@pytest.mark.usefixtures('clean_system', 'remove_test_dir')
+def test_generate_copy_without_render_extensions():
+    generate.generate_files(
+        context={
+            'cookiecutter': {
+                'repo_name': 'test_copy_without_render',
+                'render_test': 'I have been rendered!',
+                '_copy_without_render': [
+                    '*not-rendered',
+                    'rendered/not_rendered.yml',
+                    '*.txt',
+                ]}
+        },
+        repo_dir='tests/test-generate-copy-without-render'
+    )
+
+    dir_contents = os.listdir('test_copy_without_render')
+
+    assert '{{cookiecutter.repo_name}}-not-rendered' in dir_contents
+    assert 'test_copy_without_render-rendered' in dir_contents
+
+    with open('test_copy_without_render/README.txt') as f:
+        assert '{{cookiecutter.render_test}}' in f.read()
+
+    with open('test_copy_without_render/README.rst') as f:
+        assert 'I have been rendered!' in f.read()
+
+    with open('test_copy_without_render/'
+              'test_copy_without_render-rendered/'
+              'README.txt') as f:
+        assert '{{cookiecutter.render_test}}' in f.read()
+
+    with open('test_copy_without_render/'
+              'test_copy_without_render-rendered/'
+              'README.rst') as f:
+        assert 'I have been rendered' in f.read()
+
+    with open('test_copy_without_render/'
+              '{{cookiecutter.repo_name}}-not-rendered/'
+              'README.rst') as f:
+        assert '{{cookiecutter.render_test}}' in f.read()
+
+    with open('test_copy_without_render/rendered/not_rendered.yml') as f:
+        assert '{{cookiecutter.render_test}}' in f.read()
diff --git a/tests/test_prompt.py b/tests/test_prompt.py
index 8880111..1bb8e17 100644
--- a/tests/test_prompt.py
+++ b/tests/test_prompt.py
@@ -43,62 +43,73 @@ class TestPrompt(object):
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'Audrey Roy'
         )
-        context = {"cookiecutter": {"full_name": "Your Name"}}
+        context = {'cookiecutter': {'full_name': 'Your Name'}}
 
         cookiecutter_dict = prompt.prompt_for_config(context)
-        assert cookiecutter_dict == {"full_name": u"Audrey Roy"}
+        assert cookiecutter_dict == {'full_name': u'Audrey Roy'}
 
     def test_prompt_for_config_unicode(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'Pizzä ïs Gööd'
         )
-        context = {"cookiecutter": {"full_name": "Your Name"}}
+        context = {'cookiecutter': {'full_name': 'Your Name'}}
 
         cookiecutter_dict = prompt.prompt_for_config(context)
-        assert cookiecutter_dict == {"full_name": u"Pizzä ïs Gööd"}
+        assert cookiecutter_dict == {'full_name': u'Pizzä ïs Gööd'}
 
     def test_unicode_prompt_for_config_unicode(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'Pizzä ïs Gööd'
         )
-        context = {"cookiecutter": {"full_name": u"Řekni či napiš své jméno"}}
+        context = {'cookiecutter': {'full_name': u'Řekni či napiš své jméno'}}
 
         cookiecutter_dict = prompt.prompt_for_config(context)
-        assert cookiecutter_dict == {"full_name": u"Pizzä ïs Gööd"}
+        assert cookiecutter_dict == {'full_name': u'Pizzä ïs Gööd'}
 
     def test_unicode_prompt_for_default_config_unicode(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'\n'
         )
-        context = {"cookiecutter": {"full_name": u"Řekni či napiš své jméno"}}
+        context = {'cookiecutter': {'full_name': u'Řekni či napiš své jméno'}}
 
         cookiecutter_dict = prompt.prompt_for_config(context)
-        assert cookiecutter_dict == {"full_name": u"Řekni či napiš své jméno"}
+        assert cookiecutter_dict == {'full_name': u'Řekni či napiš své jméno'}
 
     def test_unicode_prompt_for_templated_config(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'\n'
         )
-        context = {"cookiecutter": OrderedDict([
+        context = {'cookiecutter': OrderedDict([
             (
-                "project_name",
-                u"A New Project"
+                'project_name',
+                u'A New Project'
             ), (
-                "pkg_name",
-                u"{{ cookiecutter.project_name|lower|replace(' ', '') }}"
+                'pkg_name',
+                u'{{ cookiecutter.project_name|lower|replace(" ", "") }}'
             )
         ])}
 
         exp_cookiecutter_dict = {
-            "project_name": u"A New Project", "pkg_name": u"anewproject"
+            'project_name': u'A New Project', 'pkg_name': u'anewproject'
         }
         cookiecutter_dict = prompt.prompt_for_config(context)
         assert cookiecutter_dict == exp_cookiecutter_dict
 
+    def test_dont_prompt_for_private_context_var(self, monkeypatch):
+        monkeypatch.setattr(
+            'cookiecutter.prompt.read_response',
+            lambda x: pytest.fail(
+                'Should not try to read a response for private context var'
+            )
+        )
+        context = {'cookiecutter': {'_copy_without_render': ['*.html']}}
+        cookiecutter_dict = prompt.prompt_for_config(context)
+        assert cookiecutter_dict == {'_copy_without_render': ['*.html']}
+
 
 class TestQueryAnswers(object):
     @pytest.fixture(params=[u'y', u'ye', u'yes'])
@@ -110,14 +121,14 @@ class TestQueryAnswers(object):
 
     @pytest.mark.usefixtures('patch_read_response')
     def test_query(self):
-        assert prompt.query_yes_no("Blah?")
+        assert prompt.query_yes_no('Blah?')
 
     def test_query_n(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'n'
         )
-        assert not prompt.query_yes_no("Blah?")
+        assert not prompt.query_yes_no('Blah?')
 
 
 class TestQueryDefaults(object):
@@ -126,21 +137,21 @@ class TestQueryDefaults(object):
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'y'
         )
-        assert prompt.query_yes_no("Blah?", default=None)
+        assert prompt.query_yes_no('Blah?', default=None)
 
     def test_query_n_none_default(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u'n'
         )
-        assert not prompt.query_yes_no("Blah?", default=None)
+        assert not prompt.query_yes_no('Blah?', default=None)
 
     def test_query_no_default(self, monkeypatch):
         monkeypatch.setattr(
             'cookiecutter.prompt.read_response',
             lambda x=u'': u''
         )
-        assert not prompt.query_yes_no("Blah?", default='no')
+        assert not prompt.query_yes_no('Blah?', default='no')
 
     def test_query_bad_default(self, monkeypatch):
         monkeypatch.setattr(
@@ -148,5 +159,4 @@ class TestQueryDefaults(object):
             lambda x=u'': u'junk'
         )
         with pytest.raises(ValueError):
-            prompt.query_yes_no("Blah?", default='yn')
-
+            prompt.query_yes_no('Blah?', default='yn')