Skip to content

Commit

Permalink
Add support for symlinks in cookiecutter templates for posix systems
Browse files Browse the repository at this point in the history
 - Support copying symlinks as symlinks for both rendered and
non-rendered directories
 - Support symlinks that link to a rendered directory
 - Support symlinks that get rendered

Skip symlink test on win+py < 3.2

Enable symlinks for Appveyor + flake8ify
  • Loading branch information
pjbull committed Apr 23, 2017
1 parent f9887ca commit ac34b6e
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 4 deletions.
1 change: 1 addition & 0 deletions appveyor.yml
Expand Up @@ -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\'))\""

Expand Down
27 changes: 23 additions & 4 deletions cookiecutter/generate.py
Expand Up @@ -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)
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions 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"
]
}
Empty file.
Empty file.
Empty file.
112 changes: 112 additions & 0 deletions 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 }}')

0 comments on commit ac34b6e

Please sign in to comment.