diff --git a/appveyor.yml b/appveyor.yml index 70f44a54a..1d73f46fc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,6 +31,7 @@ environment: init: - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% - "git config --system http.sslcainfo \"C:\\Program Files\\Git\\mingw64\\ssl\\certs\\ca-bundle.crt\"" + - "git config --global core.symlinks true" - "%PYTHON%/python -V" - "%PYTHON%/python -c \"import struct;print(8 * struct.calcsize(\'P\'))\"" diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 4d2f1eb0a..4214d6675 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -181,7 +181,7 @@ def generate_file(project_dir, infile, context, env): def render_and_create_dir(dirname, context, output_dir, environment, - overwrite_if_exists=False): + overwrite_if_exists=False, symlink=None): """Render name of a directory, create the directory, return its path.""" name_tmpl = environment.from_string(dirname) rendered_dirname = name_tmpl.render(**context) @@ -208,7 +208,19 @@ def render_and_create_dir(dirname, context, output_dir, environment, msg = 'Error: "{}" directory already exists'.format(dir_to_create) raise OutputDirExistsException(msg) - make_sure_path_exists(dir_to_create) + if symlink is not None: + link_tmpl = environment.from_string(symlink) + rendered_link = link_tmpl.render(**context) + + logger.debug('Creating symlink from {} to {}'.format( + dir_to_create, + rendered_link + )) + + os.symlink(rendered_link, dir_to_create) + else: + make_sure_path_exists(dir_to_create) + return dir_to_create @@ -293,9 +305,15 @@ def generate_files(repo_dir, context=None, output_dir='.', # unrendered directories, since they will just be copied. copy_dirs = [] render_dirs = [] + symlinks = dict() for d in dirs: d_ = os.path.normpath(os.path.join(root, d)) + + if os.path.islink(d_): + logger.debug('Processing symlink at {}...'.format(d)) + symlinks[d] = os.readlink(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 @@ -311,7 +329,7 @@ def generate_files(repo_dir, context=None, output_dir='.', 'Copying dir {} to {} without rendering' ''.format(indir, outdir) ) - shutil.copytree(indir, outdir) + shutil.copytree(indir, outdir, symlinks=True) # We mutate ``dirs``, because we only want to go through these dirs # recursively @@ -324,7 +342,8 @@ def generate_files(repo_dir, context=None, output_dir='.', context, output_dir, env, - overwrite_if_exists + overwrite_if_exists, + symlink=symlinks.get(d, None) ) except UndefinedError as err: rmtree(project_dir) diff --git a/tests/test-generate-symlinks/cookiecutter.json b/tests/test-generate-symlinks/cookiecutter.json new file mode 100644 index 000000000..d8f906797 --- /dev/null +++ b/tests/test-generate-symlinks/cookiecutter.json @@ -0,0 +1,9 @@ +{ + "name": "test_symlinks", + "link_dir": "rendered_dir", + "sym_to_nontemp": "rendered_sym_to_original", + "sym_to_temp": "rendered_sym_to_rendered_dir", + "_copy_without_render": [ + "copy_no_render" + ] +} diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/original/afile.txt b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/original/afile.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink new file mode 120000 index 000000000..94f3610c0 --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink @@ -0,0 +1 @@ +original \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink_to_rendered b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink_to_rendered new file mode 120000 index 000000000..383ed85cb --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink_to_rendered @@ -0,0 +1 @@ +{{ cookiecutter.link_dir }} \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.link_dir }}/.gitkeep b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.link_dir }}/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_nontemp }} b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_nontemp }} new file mode 120000 index 000000000..1a154a6e2 --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_nontemp }} @@ -0,0 +1 @@ +original/ \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_temp }} b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_temp }} new file mode 120000 index 000000000..1996a98d6 --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_temp }} @@ -0,0 +1 @@ +{{ cookiecutter.link_dir }}/ \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/original/afile.txt b/tests/test-generate-symlinks/{{ cookiecutter.name }}/original/afile.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink b/tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink new file mode 120000 index 000000000..94f3610c0 --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink @@ -0,0 +1 @@ +original \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink_to_rendered b/tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink_to_rendered new file mode 120000 index 000000000..383ed85cb --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink_to_rendered @@ -0,0 +1 @@ +{{ cookiecutter.link_dir }} \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.link_dir }}/.gitkeep b/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.link_dir }}/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_nontemp }} b/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_nontemp }} new file mode 120000 index 000000000..1a154a6e2 --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_nontemp }} @@ -0,0 +1 @@ +original/ \ No newline at end of file diff --git a/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_temp }} b/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_temp }} new file mode 120000 index 000000000..1996a98d6 --- /dev/null +++ b/tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_temp }} @@ -0,0 +1 @@ +{{ cookiecutter.link_dir }}/ \ No newline at end of file diff --git a/tests/test_generate_symlinks.py b/tests/test_generate_symlinks.py new file mode 100644 index 000000000..491dd06e6 --- /dev/null +++ b/tests/test_generate_symlinks.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +""" +test_generate_symlinks +--------------------------------- +""" + +from __future__ import unicode_literals +import os +import sys + +import pytest + +from cookiecutter import generate +from cookiecutter import utils + +TEST_OUTPUT_DIR = 'test_symlinks' + +WIN_BEFORE_PY32 = sys.platform.startswith('win') and sys.version_info < (3, 2) + + +@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_OUTPUT_DIR): + utils.rmtree(TEST_OUTPUT_DIR) + request.addfinalizer(fin_remove_test_dir) + + +@pytest.mark.skipif(WIN_BEFORE_PY32, + reason='No symlinks on Windows + Python < 3.2') +@pytest.mark.usefixtures('clean_system', 'remove_test_dir') +def test_generate_copy_without_render_extensions(): + generate.generate_files( + context={ + 'cookiecutter': { + 'name': TEST_OUTPUT_DIR, + "link_dir": "rendered_dir", + "sym_to_nontemp": "rendered_sym_to_original", + "sym_to_temp": "rendered_sym_to_rendered_dir", + "_copy_without_render": [ + "copy_no_render" + ] + } + }, + repo_dir='tests/test-generate-symlinks' + ) + + dir_contents = os.listdir(TEST_OUTPUT_DIR) + + assert 'copy_no_render' in dir_contents + assert 'original' in dir_contents + assert 'rendered_dir' in dir_contents + assert 'rendered_sym_to_original' in dir_contents + assert 'rendered_sym_to_rendered_dir' in dir_contents + assert 'symlink' in dir_contents + + # Test links that have been rendered and copied + def _test_symlink(root, link, points_to): + assert os.path.islink(os.path.join(root, link)) + + actual_points_to = os.readlink(os.path.join(root, link)) + + if actual_points_to.endswith(os.sep): + actual_points_to = actual_points_to[:-1] + + assert actual_points_to == points_to + + # normal symlink, not rendered target + _test_symlink(TEST_OUTPUT_DIR, 'symlink', 'original') + + # normal symlink, rendered target + _test_symlink(TEST_OUTPUT_DIR, 'symlink_to_rendered', 'rendered_dir') + + # rendered symlink, not rendered target + _test_symlink(TEST_OUTPUT_DIR, 'rendered_sym_to_original', 'original') + + # rendered symlink, rendered target + _test_symlink(TEST_OUTPUT_DIR, 'rendered_sym_to_rendered_dir', + 'rendered_dir') + + # Test links that have not been rendered + non_rendered_dir = os.path.join(TEST_OUTPUT_DIR, 'copy_no_render') + non_rendered_dir_contents = os.listdir(non_rendered_dir) + + assert 'original' in non_rendered_dir_contents + assert 'symlink' in non_rendered_dir_contents + assert 'symlink_to_rendered' in non_rendered_dir_contents + assert '{{ cookiecutter.link_dir }}' in non_rendered_dir_contents + assert '{{ cookiecutter.sym_to_nontemp }}' in non_rendered_dir_contents + assert '{{ cookiecutter.sym_to_temp }}' in non_rendered_dir_contents + + # normal symlink, not rendered target + _test_symlink(non_rendered_dir, 'symlink', 'original') + + # normal symlink, rendered target + _test_symlink(non_rendered_dir, + 'symlink_to_rendered', + '{{ cookiecutter.link_dir }}') + + # rendered symlink, not rendered target + _test_symlink(non_rendered_dir, + '{{ cookiecutter.sym_to_nontemp }}', + 'original') + + # rendered symlink, rendered target + _test_symlink(non_rendered_dir, + '{{ cookiecutter.sym_to_temp }}', + '{{ cookiecutter.link_dir }}')