From deb99f037d130ba7d0a27cc03dd75b7468296aba Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Wed, 7 Dec 2016 18:28:11 -0500 Subject: [PATCH 1/8] Add support for symlinks in cookiecutter templates for posix systems - 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 --- cookiecutter/generate.py | 26 +++- .../test-generate-symlinks/cookiecutter.json | 9 ++ .../copy_no_render/original/afile.txt | 0 .../copy_no_render/symlink | 1 + .../copy_no_render/symlink_to_rendered | 1 + .../{{ cookiecutter.link_dir }}/.gitkeep | 0 .../{{ cookiecutter.sym_to_nontemp }} | 1 + .../{{ cookiecutter.sym_to_temp }} | 1 + .../original/afile.txt | 0 .../{{ cookiecutter.name }}/symlink | 1 + .../symlink_to_rendered | 1 + .../{{ cookiecutter.link_dir }}/.gitkeep | 0 .../{{ cookiecutter.sym_to_nontemp }} | 1 + .../{{ cookiecutter.sym_to_temp }} | 1 + tests/test_generate_symlinks.py | 112 ++++++++++++++++++ 15 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 tests/test-generate-symlinks/cookiecutter.json create mode 100644 tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/original/afile.txt create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/symlink_to_rendered create mode 100644 tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.link_dir }}/.gitkeep create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_nontemp }} create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/copy_no_render/{{ cookiecutter.sym_to_temp }} create mode 100644 tests/test-generate-symlinks/{{ cookiecutter.name }}/original/afile.txt create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/symlink_to_rendered create mode 100644 tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.link_dir }}/.gitkeep create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_nontemp }} create mode 120000 tests/test-generate-symlinks/{{ cookiecutter.name }}/{{ cookiecutter.sym_to_temp }} create mode 100644 tests/test_generate_symlinks.py diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 7dbd9867b..252c8aeaf 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -199,7 +199,7 @@ def generate_file(project_dir, infile, context, env, skip_if_file_exists=False): def render_and_create_dir( - dirname, context, output_dir, environment, overwrite_if_exists=False + dirname, context, output_dir, environment, overwrite_if_exists=False, symlink=None ): """Render name of a directory, create the directory, return its path.""" name_tmpl = environment.from_string(dirname) @@ -224,6 +224,19 @@ def render_and_create_dir( else: 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, not output_dir_exists @@ -323,9 +336,15 @@ def generate_files( # 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 @@ -336,10 +355,9 @@ def generate_files( 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)) outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', 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 @@ -348,7 +366,7 @@ def generate_files( unrendered_dir = os.path.join(project_dir, root, d) try: render_and_create_dir( - unrendered_dir, context, output_dir, env, overwrite_if_exists + unrendered_dir, context, output_dir, env, overwrite_if_exists, symlink=symlinks.get(d, None) ) except UndefinedError as err: if delete_project_on_failure: 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 }}') From bc980cf134dc16f7795d99490609c2b5668fef9c Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Tue, 16 May 2017 06:12:45 +0000 Subject: [PATCH 2/8] Update tests --- tests/test_generate_symlinks.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_generate_symlinks.py b/tests/test_generate_symlinks.py index 491dd06e6..224e4009f 100644 --- a/tests/test_generate_symlinks.py +++ b/tests/test_generate_symlinks.py @@ -14,9 +14,8 @@ 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) +TEST_OUTPUT_DIR = 'test_symlinks' @pytest.fixture(scope='function') @@ -30,10 +29,8 @@ def fin_remove_test_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(): +def test_symlinks(): generate.generate_files( context={ 'cookiecutter': { @@ -110,3 +107,4 @@ def _test_symlink(root, link, points_to): _test_symlink(non_rendered_dir, '{{ cookiecutter.sym_to_temp }}', '{{ cookiecutter.link_dir }}') + From 6f38fa9737f7e1c817ea2f3de0f5938c53728c57 Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Tue, 16 May 2017 16:41:57 +0000 Subject: [PATCH 3/8] Add documentation for symlinks --- AUTHORS.md | 1 + docs/advanced/index.rst | 1 + docs/advanced/symlinks.rst | 58 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 docs/advanced/symlinks.rst diff --git a/AUTHORS.md b/AUTHORS.md index d991d54fa..13b835c30 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -170,3 +170,4 @@ * Tom Forbes ([@orf](https://github.com/orf)) * Xie Yanbo ([@xyb](https://github.com/xyb)) * Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) +* Peter Bull ([@pjbull](https://github.com/pjbull)) diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index 90e7f2933..57569eb33 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -23,3 +23,4 @@ Various advanced topics regarding cookiecutter usage. template_extensions directories new_line_characters + symlinks diff --git a/docs/advanced/symlinks.rst b/docs/advanced/symlinks.rst new file mode 100644 index 000000000..b3150bc9a --- /dev/null +++ b/docs/advanced/symlinks.rst @@ -0,0 +1,58 @@ +.. _symlinks: + +Symlinks +---------------------- + +Symlinks are virtual files or folders that simply point to another location on the +file system. For example, you may add a symlink to your project template ``system_logs`` +that points to ``/var/log`` on the system where the template is rendered. Symlinks can +be thought of as a shortcut to a specific file or folder. + +Symlinks will work on most platforms with cookiecutter. However, if you expect your +template to be used on Windows systems with Python < 3.2, it is recommended that you +do not use symlinks or that you handle symlinking in a post-generation hook. + + +Using symlinks +~~~~~~~~~~~~~~~~~~~~~~~ + +`Symbolic links`_ are commonly used in posix systems. Cookiecutter supports symlinks +in templates both as rendered and unrendered content. That is, the symlink itself +both be named with a variable or point to a destination with a variable in it. + +On posix systems (see below for Windows systems) you can simply create a symbolic link +as normal in the template directory:: + + ln -s existing_file_path new_symlink_path + +As stated above, either ``existing_file_path`` or ``new_symlink_path`` can contain +variables that will be templated in braces ``{{ cookiecutter.variable }}``. These +will be replaced when the template is rendered. + + +Symlinks on Windows +~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: Symlinks are not currently supported on Windows systems with a + Python version < 3.2. Symlinks should work as expected on other + configurations. + +Symlinks in the posix sense are a relatively new addition to Windows operating +systems, and support for these was introduced into Python in Python 3.2. If you +want to use symlinks in a template on a Windows system, you may need to take some +additional steps. On windows the ``mklink`` command will create links:: + + > mklink new_symlink_path existing_file_path + +First, if your template is cloned using ``git``, you will need to tell ``git`` to respect +symlinks when cloning the project. This can be done at the commandline:: + + $ git config --global core.symlinks true + +Second, it may be the case that a user, if not an administrator, needs to be granted +special permissions in order to create symlinks. More details on permissions for +symlinks and how those can be managed are available `from Microsoft`_. + + +.. _`Symbolic links`: https://en.wikipedia.org/wiki/Symbolic_link +.. _`from Microsoft`: https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#TXpueSdQMpMz2YWf.97 \ No newline at end of file From 00e97223ddb1c9d4ef3a4e97867ce62d02b1e3ae Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Wed, 7 Jul 2021 15:46:57 -0700 Subject: [PATCH 4/8] lint --- cookiecutter/generate.py | 14 +++++++++----- tests/test_generate_symlinks.py | 30 ++++++++++++++---------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 252c8aeaf..788466d6b 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -228,10 +228,9 @@ def render_and_create_dir( link_tmpl = environment.from_string(symlink) rendered_link = link_tmpl.render(**context) - logger.debug('Creating symlink from {} to {}'.format( - dir_to_create, - rendered_link - )) + logger.debug( + 'Creating symlink from {} to {}'.format(dir_to_create, rendered_link) + ) os.symlink(rendered_link, dir_to_create) else: @@ -366,7 +365,12 @@ def generate_files( unrendered_dir = os.path.join(project_dir, root, d) try: render_and_create_dir( - unrendered_dir, context, output_dir, env, overwrite_if_exists, symlink=symlinks.get(d, None) + unrendered_dir, + context, + output_dir, + env, + overwrite_if_exists, + symlink=symlinks.get(d, None), ) except UndefinedError as err: if delete_project_on_failure: diff --git a/tests/test_generate_symlinks.py b/tests/test_generate_symlinks.py index 224e4009f..5faae5a12 100644 --- a/tests/test_generate_symlinks.py +++ b/tests/test_generate_symlinks.py @@ -23,9 +23,11 @@ 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) @@ -38,12 +40,10 @@ def 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" - ] + "_copy_without_render": ["copy_no_render"], } }, - repo_dir='tests/test-generate-symlinks' + repo_dir='tests/test-generate-symlinks', ) dir_contents = os.listdir(TEST_OUTPUT_DIR) @@ -76,8 +76,7 @@ def _test_symlink(root, link, points_to): _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_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') @@ -94,17 +93,16 @@ def _test_symlink(root, link, points_to): _test_symlink(non_rendered_dir, 'symlink', 'original') # normal symlink, rendered target - _test_symlink(non_rendered_dir, - 'symlink_to_rendered', - '{{ cookiecutter.link_dir }}') + _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') + _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 }}') - + _test_symlink( + non_rendered_dir, + '{{ cookiecutter.sym_to_temp }}', + '{{ cookiecutter.link_dir }}', + ) From 348781cc0aebbf82df1e89a28e7e23c20ad9c544 Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Wed, 7 Jul 2021 15:49:37 -0700 Subject: [PATCH 5/8] add back missing line --- cookiecutter/generate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 788466d6b..99e9d1c6f 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -354,6 +354,7 @@ def generate_files( 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)) outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', indir, outdir) shutil.copytree(indir, outdir, symlinks=True) From e04f6b45c49a2cbac326e169835cd85902f402a4 Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Wed, 7 Jul 2021 16:00:55 -0700 Subject: [PATCH 6/8] Update docstrings and linting --- docs/advanced/symlinks.rst | 11 +++-------- tests/test_generate_symlinks.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/docs/advanced/symlinks.rst b/docs/advanced/symlinks.rst index b3150bc9a..8cc2ab265 100644 --- a/docs/advanced/symlinks.rst +++ b/docs/advanced/symlinks.rst @@ -9,8 +9,7 @@ that points to ``/var/log`` on the system where the template is rendered. Symlin be thought of as a shortcut to a specific file or folder. Symlinks will work on most platforms with cookiecutter. However, if you expect your -template to be used on Windows systems with Python < 3.2, it is recommended that you -do not use symlinks or that you handle symlinking in a post-generation hook. +template to be used on Windows systems, see the additional information below. Using symlinks @@ -33,10 +32,6 @@ will be replaced when the template is rendered. Symlinks on Windows ~~~~~~~~~~~~~~~~~~~~~~~ -.. warning:: Symlinks are not currently supported on Windows systems with a - Python version < 3.2. Symlinks should work as expected on other - configurations. - Symlinks in the posix sense are a relatively new addition to Windows operating systems, and support for these was introduced into Python in Python 3.2. If you want to use symlinks in a template on a Windows system, you may need to take some @@ -44,7 +39,7 @@ additional steps. On windows the ``mklink`` command will create links:: > mklink new_symlink_path existing_file_path -First, if your template is cloned using ``git``, you will need to tell ``git`` to respect +First, if your template is cloned using ``git``, you may need to tell ``git`` to respect symlinks when cloning the project. This can be done at the commandline:: $ git config --global core.symlinks true @@ -55,4 +50,4 @@ symlinks and how those can be managed are available `from Microsoft`_. .. _`Symbolic links`: https://en.wikipedia.org/wiki/Symbolic_link -.. _`from Microsoft`: https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#TXpueSdQMpMz2YWf.97 \ No newline at end of file +.. _`from Microsoft`: https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#TXpueSdQMpMz2YWf.97 diff --git a/tests/test_generate_symlinks.py b/tests/test_generate_symlinks.py index 5faae5a12..7766176ee 100644 --- a/tests/test_generate_symlinks.py +++ b/tests/test_generate_symlinks.py @@ -1,13 +1,7 @@ -# -*- coding: utf-8 -*- - -""" -test_generate_symlinks ---------------------------------- -""" +"""Tests for `generate.py` that have symlinks in the templates.""" from __future__ import unicode_literals import os -import sys import pytest @@ -20,9 +14,7 @@ @pytest.fixture(scope='function') def remove_test_dir(request): - """ - Remove the folder that is created by the test. - """ + """Remove the folder that is created by the test.""" def fin_remove_test_dir(): if os.path.exists(TEST_OUTPUT_DIR): @@ -33,6 +25,11 @@ def fin_remove_test_dir(): @pytest.mark.usefixtures('clean_system', 'remove_test_dir') def test_symlinks(): + """ + Verify generating projects with symlinks. + + Includes both rendered and non-rendered symlink paths. + """ generate.generate_files( context={ 'cookiecutter': { From 52bea80f82c2e82c2de2ceae3c10f00bcf23a159 Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Wed, 7 Jul 2021 16:49:04 -0700 Subject: [PATCH 7/8] passing tests --- cookiecutter/generate.py | 11 ++++++++--- tests/test_generate_symlinks.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 99e9d1c6f..301513dd6 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -218,11 +218,14 @@ def render_and_create_dir( logger.debug( 'Output directory %s already exists, overwriting it', dir_to_create ) + + # must be removed for symlink to be created successfully + if symlink is not None: + shutil.rmtree(dir_to_create) + else: msg = 'Error: "{}" directory already exists'.format(dir_to_create) raise OutputDirExistsException(msg) - else: - make_sure_path_exists(dir_to_create) if symlink is not None: link_tmpl = environment.from_string(symlink) @@ -357,7 +360,9 @@ def generate_files( outdir = os.path.normpath(os.path.join(project_dir, indir)) outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', indir, outdir) - shutil.copytree(indir, outdir, symlinks=True) + shutil.copytree( + indir, outdir, symlinks=True, dirs_exist_ok=overwrite_if_exists + ) # We mutate ``dirs``, because we only want to go through these dirs # recursively diff --git a/tests/test_generate_symlinks.py b/tests/test_generate_symlinks.py index 7766176ee..0a0012b16 100644 --- a/tests/test_generate_symlinks.py +++ b/tests/test_generate_symlinks.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import os +import shutil import pytest @@ -103,3 +104,29 @@ def _test_symlink(root, link, points_to): '{{ cookiecutter.sym_to_temp }}', '{{ cookiecutter.link_dir }}', ) + + # test overwriting + symlinks + utils.rmtree(TEST_OUTPUT_DIR) # remove output + os.makedirs(os.path.join(TEST_OUTPUT_DIR, "symlink")) + shutil.copy( + os.path.join("tests", "test-generate-symlinks", "cookiecutter.json"), + os.path.join( + TEST_OUTPUT_DIR, "symlink", "afile.txt" + ), # copy to where output will exist + ) + 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=os.path.join('tests', 'test-generate-symlinks'), + overwrite_if_exists=True, # overwrite the symlink + ) + + # normal symlink, not rendered target + _test_symlink(non_rendered_dir, 'symlink', 'original') From ae4d1eee506ddd9ace4b222b0a0addc1ed22b1c1 Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Thu, 8 Jul 2021 08:28:42 -0700 Subject: [PATCH 8/8] remove python 3.8 feature --- cookiecutter/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 301513dd6..146402887 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -361,7 +361,7 @@ def generate_files( outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', indir, outdir) shutil.copytree( - indir, outdir, symlinks=True, dirs_exist_ok=overwrite_if_exists + indir, outdir, symlinks=True, ) # We mutate ``dirs``, because we only want to go through these dirs