Skip to content

Commit

Permalink
Refactored prompt_and_delete, and added option to use old repo copy.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Sep 16, 2017
1 parent 25008ab commit 3cac20c
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 260 deletions.
42 changes: 42 additions & 0 deletions cookiecutter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import os
import stat
import shutil
import sys

from .prompt import read_user_yes_no

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -79,3 +82,42 @@ def make_executable(script_path):
"""
status = os.stat(script_path)
os.chmod(script_path, status.st_mode | stat.S_IEXEC)


def prompt_and_delete(path, no_input=False):
"""Ask the user whether it's okay to delete the previously-downloaded
file/directory.
If yes, deletes it. If no, checks to see if the old version should be
reused. If yes, it's reused; otherwise, Cookiecutter exits.
:param path: Previously downloaded zipfile.
:param no_input: Suppress prompt to delete repo and just delete it.
:return: True if the content was deleted
"""
# Suppress prompt if called via API
if no_input:
ok_to_delete = True
else:
question = (
"You've downloaded {} before. "
"Is it okay to delete and re-download it?"
).format(path)

ok_to_delete = read_user_yes_no(question, 'yes')

if ok_to_delete:
if os.path.isdir(path):
rmtree(path)
else:
os.remove(path)
return True
else:
ok_to_reuse = read_user_yes_no(
"Do you want to re-use the existing version?", 'yes'
)

if ok_to_reuse:
return False

sys.exit()
79 changes: 28 additions & 51 deletions cookiecutter/vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
from .exceptions import (
RepositoryNotFound, RepositoryCloneFailed, UnknownRepoType, VCSNotInstalled
)
from .prompt import read_user_yes_no
from .utils import make_sure_path_exists, rmtree
from .utils import make_sure_path_exists, prompt_and_delete

logger = logging.getLogger(__name__)

Expand All @@ -25,31 +24,6 @@
]


def prompt_and_delete_repo(repo_dir, no_input=False):
"""Ask the user whether it's okay to delete the previously-cloned repo.
If yes, deletes it. Otherwise, Cookiecutter exits.
:param repo_dir: Directory of previously-cloned repo.
:param no_input: Suppress prompt to delete repo and just delete it.
"""
# Suppress prompt if called via API
if no_input:
ok_to_delete = True
else:
question = (
"You've cloned {} before. "
"Is it okay to delete and re-clone it?"
).format(repo_dir)

ok_to_delete = read_user_yes_no(question, 'yes')

if ok_to_delete:
rmtree(repo_dir)
else:
sys.exit()


def identify_repo(repo_url):
"""Determine if `repo_url` should be treated as a URL to a git or hg repo.
Expand Down Expand Up @@ -114,32 +88,35 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False):
logger.debug('repo_dir is {0}'.format(repo_dir))

if os.path.isdir(repo_dir):
prompt_and_delete_repo(repo_dir, no_input=no_input)

try:
subprocess.check_output(
[repo_type, 'clone', repo_url],
cwd=clone_to_dir,
stderr=subprocess.STDOUT,
)
if checkout is not None:
clone = prompt_and_delete(repo_dir, no_input=no_input)
else:
clone = True

if clone:
try:
subprocess.check_output(
[repo_type, 'checkout', checkout],
cwd=repo_dir,
[repo_type, 'clone', repo_url],
cwd=clone_to_dir,
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as clone_error:
output = clone_error.output.decode('utf-8')
if 'not found' in output.lower():
raise RepositoryNotFound(
'The repository {} could not be found, '
'have you made a typo?'.format(repo_url)
)
if any(error in output for error in BRANCH_ERRORS):
raise RepositoryCloneFailed(
'The {} branch of repository {} could not found, '
'have you made a typo?'.format(checkout, repo_url)
)
raise
if checkout is not None:
subprocess.check_output(
[repo_type, 'checkout', checkout],
cwd=repo_dir,
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as clone_error:
output = clone_error.output.decode('utf-8')
if 'not found' in output.lower():
raise RepositoryNotFound(
'The repository {} could not be found, '
'have you made a typo?'.format(repo_url)
)
if any(error in output for error in BRANCH_ERRORS):
raise RepositoryCloneFailed(
'The {} branch of repository {} could not found, '
'have you made a typo?'.format(checkout, repo_url)
)
raise

return repo_dir
51 changes: 12 additions & 39 deletions cookiecutter/zipfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,7 @@
from zipfile import BadZipfile as BadZipFile

from .exceptions import InvalidZipRepository
from .prompt import read_user_yes_no
from .utils import make_sure_path_exists, rmtree


def prompt_and_delete(path, no_input=False):
"""Ask the user whether it's okay to delete the previously-downloaded
file/directory.
If yes, deletes it. Otherwise, Cookiecutter exits.
:param path: Previously downloaded zipfile.
:param no_input: Suppress prompt to delete repo and just delete it.
"""
# Suppress prompt if called via API
if no_input:
ok_to_delete = True
else:
question = (
"You've downloaded {} before. "
"Is it okay to delete and re-download it?"
).format(path)

ok_to_delete = read_user_yes_no(question, 'yes')

if ok_to_delete:
if os.path.isdir(path):
rmtree(path)
else:
os.remove(path)
else:
sys.exit()
from .utils import make_sure_path_exists, prompt_and_delete


def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False):
Expand All @@ -67,14 +37,17 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False):
zip_path = os.path.join(clone_to_dir, identifier)

if os.path.exists(zip_path):
prompt_and_delete(zip_path, no_input=no_input)

# (Re) download the zipfile
r = requests.get(zip_uri, stream=True)
with open(zip_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
download = prompt_and_delete(zip_path, no_input=no_input)
else:
download = True

if download:
# (Re) download the zipfile
r = requests.get(zip_uri, stream=True)
with open(zip_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
else:
# Just use the local zipfile as-is.
zip_path = os.path.abspath(zip_uri)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cookiecutter_local_no_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_cookiecutter_template_cleanup(mocker):
)

mock_tmpdir = mocker.patch(
'cookiecutter.zipfile.prompt_and_delete',
'cookiecutter.utils.prompt_and_delete',
return_value=True,
autospec=True
)
Expand Down
118 changes: 118 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,121 @@ def test_work_in():
# Make sure that exceptions are still bubbled up
with pytest.raises(TestException):
test_work_in()


def test_prompt_should_ask_and_rm_repo_dir(mocker, tmpdir):
"""In `prompt_and_delete()`, if the user agrees to delete/reclone the
repo, the repo should be deleted.
"""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no',
return_value=True,
autospec=True
)
repo_dir = tmpdir.mkdir('repo')

deleted = utils.prompt_and_delete(str(repo_dir))

assert mock_read_user.called
assert not repo_dir.exists()
assert deleted


def test_prompt_should_ask_and_rm_repo_file(mocker, tmpdir):
"""In `prompt_and_delete()`, if the user agrees to delete/reclone a
repo file, the repo should be deleted.
"""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no',
return_value=True,
autospec=True
)

repo_file = tmpdir.join('repo.zip')
repo_file.write('this is zipfile content')

deleted = utils.prompt_and_delete(str(repo_file))

assert mock_read_user.called
assert not repo_file.exists()
assert deleted


def test_prompt_should_ask_and_keep_repo_on_no_reuse(mocker, tmpdir):
"""In `prompt_and_delete()`, if the user wants to keep their old
cloned template repo, it should not be deleted.
"""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no',
return_value=False,
autospec=True
)
repo_dir = tmpdir.mkdir('repo')

with pytest.raises(SystemExit):
utils.prompt_and_delete(str(repo_dir))

assert mock_read_user.called
assert repo_dir.exists()


def test_prompt_should_ask_and_keep_repo_on_reuse(mocker, tmpdir):
"""In `prompt_and_delete()`, if the user wants to keep their old
cloned template repo, it should not be deleted.
"""
def answer(question, default):
if 'okay to delete' in question:
return False
else:
return True

mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no',
side_effect=answer,
autospec=True
)
repo_dir = tmpdir.mkdir('repo')

deleted = utils.prompt_and_delete(str(repo_dir))

assert mock_read_user.called
assert repo_dir.exists()
assert not deleted


def test_prompt_should_not_ask_if_no_input_and_rm_repo_dir(mocker, tmpdir):
"""In `prompt_and_delete()`, if `no_input` is True, the call to
`prompt.read_user_yes_no()` should be suppressed.
"""
mock_read_user = mocker.patch(
'cookiecutter.prompt.read_user_yes_no',
return_value=True,
autospec=True
)
repo_dir = tmpdir.mkdir('repo')

deleted = utils.prompt_and_delete(str(repo_dir), no_input=True)

assert not mock_read_user.called
assert not repo_dir.exists()
assert deleted


def test_prompt_should_not_ask_if_no_input_and_rm_repo_file(mocker, tmpdir):
"""In `prompt_and_delete()`, if `no_input` is True, the call to
`prompt.read_user_yes_no()` should be suppressed.
"""
mock_read_user = mocker.patch(
'cookiecutter.prompt.read_user_yes_no',
return_value=True,
autospec=True
)

repo_file = tmpdir.join('repo.zip')
repo_file.write('this is zipfile content')

deleted = utils.prompt_and_delete(str(repo_file), no_input=True)

assert not mock_read_user.called
assert not repo_file.exists()
assert deleted
4 changes: 2 additions & 2 deletions tests/vcs/test_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, tmpdir):
return_value=True
)
mocker.patch(
'cookiecutter.vcs.prompt_and_delete_repo',
'cookiecutter.vcs.prompt_and_delete',
side_effect=SystemExit,
autospec=True
)
Expand All @@ -80,7 +80,7 @@ def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, tmpdir):

clone_to_dir = tmpdir.mkdir('clone')

# Create repo_dir to trigger prompt_and_delete_repo
# Create repo_dir to trigger prompt_and_delete
clone_to_dir.mkdir('cookiecutter-pytest-plugin')

repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git'
Expand Down
Loading

0 comments on commit 3cac20c

Please sign in to comment.