From 23182a564d19f5de753a58e70ab47f7c008ce0a5 Mon Sep 17 00:00:00 2001 From: Maciej Patro Date: Sat, 7 May 2022 01:35:35 +0200 Subject: [PATCH] Add CLI option to keep project files on failure. Fixes #1631 --- cookiecutter/cli.py | 7 +++++++ cookiecutter/generate.py | 5 ++++- cookiecutter/main.py | 4 ++++ tests/test_cli.py | 9 +++++++++ tests/test_generate_files.py | 12 ++++++++++++ tests/test_specify_output_dir.py | 2 ++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index a792fa5f5..208901a92 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -144,6 +144,11 @@ def list_installed_templates(default_config, passed_config_file): @click.option( '-l', '--list-installed', is_flag=True, help='List currently installed templates.' ) +@click.option( + '--keep-project-on-failure', + is_flag=True, + help='Do not delete project folder on failure', +) def main( template, extra_context, @@ -161,6 +166,7 @@ def main( accept_hooks, replay_file, list_installed, + keep_project_on_failure, ): """Create a project from a Cookiecutter project template (TEMPLATE). @@ -205,6 +211,7 @@ def main( directory=directory, skip_if_file_exists=skip_if_file_exists, accept_hooks=_accept_hooks, + keep_project_on_failure=keep_project_on_failure, ) except ( ContextDecodingException, diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 7bdce5a8b..cd7d34df2 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -268,6 +268,7 @@ def generate_files( overwrite_if_exists=False, skip_if_file_exists=False, accept_hooks=True, + keep_project_on_failure=False, ): """Render the templates and saves them to files. @@ -277,6 +278,8 @@ def generate_files( :param overwrite_if_exists: Overwrite the contents of the output directory if it exists. :param accept_hooks: Accept pre and post hooks if set to `True`. + :param keep_project_on_failure: If `True` keep generated project directory even when + generation fails """ template_dir = find_template(repo_dir) logger.debug('Generating project from %s...', template_dir) @@ -307,7 +310,7 @@ def generate_files( # if we created the output directory, then it's ok to remove it # if rendering fails - delete_project_on_failure = output_directory_created + delete_project_on_failure = output_directory_created and not keep_project_on_failure if accept_hooks: _run_hook_from_repo_dir( diff --git a/cookiecutter/main.py b/cookiecutter/main.py index bc2f262df..64a686ad1 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -34,6 +34,7 @@ def cookiecutter( directory=None, skip_if_file_exists=False, accept_hooks=True, + keep_project_on_failure=False, ): """ Run Cookiecutter just as if using it from the command line. @@ -53,6 +54,8 @@ def cookiecutter( :param password: The password to use when extracting the repository. :param directory: Relative path to a cookiecutter template in a repository. :param accept_hooks: Accept pre and post hooks if set to `True`. + :param keep_project_on_failure: If `True` keep generated project directory even when + generation fails """ if replay and ((no_input is not False) or (extra_context is not None)): err_msg = ( @@ -118,6 +121,7 @@ def cookiecutter( skip_if_file_exists=skip_if_file_exists, output_dir=output_dir, accept_hooks=accept_hooks, + keep_project_on_failure=keep_project_on_failure, ) # Cleanup (if required) diff --git a/tests/test_cli.py b/tests/test_cli.py index ad6abd1e0..1bc2fdd55 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -109,6 +109,7 @@ def test_cli_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -135,6 +136,7 @@ def test_cli_replay_file(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -170,6 +172,7 @@ def test_cli_exit_on_noinput_and_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -205,6 +208,7 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay( password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -261,6 +265,7 @@ def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -305,6 +310,7 @@ def test_user_config(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -335,6 +341,7 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -360,6 +367,7 @@ def test_default_user_config(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -629,6 +637,7 @@ def test_cli_accept_hooks( directory=None, skip_if_file_exists=False, accept_hooks=expected, + keep_project_on_failure=False, ) diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 4d6ef1113..9cf4929aa 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -390,6 +390,18 @@ def test_raise_undefined_variable_dir_name(output_dir, undefined_context): assert not Path(output_dir).joinpath('testproject').exists() +def test_keep_project_dir_on_failure(output_dir, undefined_context): + """Verify correct error raised when directory name cannot be rendered.""" + with pytest.raises(exceptions.UndefinedVariableInTemplate): + generate.generate_files( + repo_dir='tests/undefined-variable/dir-name/', + output_dir=output_dir, + context=undefined_context, + keep_project_on_failure=True, + ) + assert Path(output_dir).joinpath('testproject').exists() + + def test_raise_undefined_variable_dir_name_existing_project( output_dir, undefined_context ): diff --git a/tests/test_specify_output_dir.py b/tests/test_specify_output_dir.py index 56c9eda6a..c907f2855 100644 --- a/tests/test_specify_output_dir.py +++ b/tests/test_specify_output_dir.py @@ -57,6 +57,7 @@ def test_api_invocation(mocker, template, output_dir, context): skip_if_file_exists=False, output_dir=output_dir, accept_hooks=True, + keep_project_on_failure=False, ) @@ -73,4 +74,5 @@ def test_default_output_dir(mocker, template, context): skip_if_file_exists=False, output_dir='.', accept_hooks=True, + keep_project_on_failure=False, )