8000 Add option to use templates from Zip files or Zip URLs by freakboy3742 · Pull Request #961 · cookiecutter/cookiecutter · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add option to use templates from Zip files or Zip URLs #961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Oct 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6ca3f32
Added support for downloading and templating from Zip files.
freakboy3742 Jun 17, 2017
9bf5215
Added docs for zipfile templates.
freakboy3742 Jun 17, 2017
9d22ecd
Fixed flake8 problems.
freakboy3742 Jun 17, 2017
30c4e95
Fixes for Python 2.7 compatibility.
freakboy3742 Jun 17, 2017
6880882
Merge branch 'master' into zipfile
freakboy3742 Sep 14, 2017
e139b29
Corrected docstring.
freakboy3742 Sep 14, 2017
cc799c0
Remove shadowing of builtins.
freakboy3742 Sep 15, 2017
cbbff9e
Add docstring for unzip method.
freakboy3742 Sep 15, 2017
cf5c1e7
Use pytest.mark.parametrize instead of a fixture.
freakboy3742 Sep 15, 2017
5786603
Improved testing of zip file extraction problems.
freakboy3742 Sep 15, 2017
25008ab
After unrolling a zipfile template, delete the extracted files.
freakboy3742 Sep 15, 2017
3cac20c
Refactored prompt_and_delete, and added option to use old repo copy.
freakboy3742 Sep 16, 2017
e72e8c8
Added handling for password-protected repositories.
freakboy3742 Sep 16, 2017
f637516
Enable password to be passed in as a paraneter.
freakboy3742 Sep 16, 2017
24420ad
Fixed flake8 problems.
freakboy3742 Sep 16, 2017
0e152ce
Minor change for Windows compatibility.
freakboy3742 Sep 16, 2017
508bc4b
Another Windows compatibility fix.
freakboy3742 Sep 16, 2017
d2e2594
Allow for capitalized file extensions (esp for Windows)
freakboy3742 Sep 22, 2017
8668961
Catch invalid zip files at the CLI level.
freakboy3742 Sep 22, 2017
3936a80
Added note explaining the deprecation import.
freakboy3742 Sep 22, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cookiecutter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
FailedHookException,
UndefinedVariableInTemplate,
UnknownExtension,
InvalidZipRepository,
RepositoryNotFound,
RepositoryCloneFailed
)
Expand Down Expand Up @@ -116,11 +117,13 @@ def main(
output_dir=output_dir,
config_file=config_file,
default_config=default_config,
password=os.environ.get('COOKIECUTTER_REPO_PASSWORD')
)
except (OutputDirExistsException,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to catch InvalidZipRepository exceptions here.

InvalidModeException,
FailedHookException,
UnknownExtension,
InvalidZipRepository,
RepositoryNotFound,
RepositoryCloneFailed) as e:
click.echo(e)
Expand Down
7 changes: 7 additions & 0 deletions cookiecutter/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,10 @@ class RepositoryNotFound(CookiecutterException):

class RepositoryCloneFailed(CookiecutterException):
"""Raised when a cookiecutter template can't be cloned."""


class InvalidZipRepository(CookiecutterException):
"""
Raised when the specified cookiecutter repository isn't a valid
Zip archive.
"""
15 changes: 12 additions & 3 deletions cookiecutter/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
from .prompt import prompt_for_config
from .replay import dump, load
from .repository import determine_repo_dir
from .utils import rmtree

logger = logging.getLogger(__name__)


def cookiecutter(
template, checkout=None, no_input=False, extra_context=None,
replay=False, overwrite_if_exists=False, output_dir='.',
config_file=None, default_config=False):
config_file=None, default_config=False, password=None):
"""
API equivalent to using Cookiecutter at the command line.

Expand All @@ -39,6 +40,7 @@ def cookiecutter(
:param output_dir: Where to output the generated project dir into.
:param config_file: User configuration file path.
:param default_config: Use default values rather than a config file.
:param password: The password to use when extracting the repository.
"""
if replay and ((no_input is not False) or (extra_context is not None)):
err_msg = (
Expand All @@ -52,12 +54,13 @@ def cookiecutter(
default_config=default_config,
)

repo_dir = determine_repo_dir(
repo_dir, cleanup = determine_repo_dir(
template=template,
abbreviations=config_dict['abbreviations'],
clone_to_dir=config_dict['cookiecutters_dir'],
checkout=checkout,
no_input=no_input,
password=password
)

template_name = os.path.basename(os.path.abspath(repo_dir))
Expand All @@ -84,9 +87,15 @@ def cookiecutter(
dump(config_dict['replay_dir'], template_name, context)

# Create project from local context and project template.
return generate_files(
result = generate_files(
repo_dir=repo_dir,
context=context,
overwrite_if_exists=overwrite_if_exists,
output_dir=output_dir
)

# Cleanup (if required)
if cleanup:
rmtree(repo_dir)

return result
9 changes: 9 additions & 0 deletions cookiecutter/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def read_user_yes_no(question, default_value):
)


def read_repo_password(question):
"""Prompt the user to enter a password

:param str question: Question to the user
"""
# Please see http://click.pocoo.org/4/api/#click.prompt
return click.prompt(question, hide_input=True)


def read_user_choice(var_name, options):
"""Prompt the user to choose from several options for the given variable.

Expand Down
29 changes: 25 additions & 4 deletions cookiecutter/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .exceptions import RepositoryNotFound
from .vcs import clone
from .zipfile import unzip

REPO_REGEX = re.compile(r"""
(?x)
Expand All @@ -23,6 +24,11 @@ def is_repo_url(value):
return bool(REPO_REGEX.match(value))


def is_zip_file(value):
"""Return True if value is a zip file."""
return value.lower().endswith('.zip')


def expand_abbreviations(template, abbreviations):
"""Expand abbreviations in a template name.

Expand Down Expand Up @@ -56,7 +62,7 @@ def repository_has_cookiecutter_json(repo_directory):


def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
no_input):
no_input, password=None):
"""
Locate the repository directory from a template reference.

Expand All @@ -71,28 +77,43 @@ def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
:param clone_to_dir: The directory to clone the repository into.
:param checkout: The branch, tag or commit ID to checkout after clone.
:param no_input: Prompt the user at command line for manual configuration?
:return: The cookiecutter template directory
:param password: The password to use when extracting the repository.
:return: A tuple containing the cookiecutter template directory, and
a boolean descriving whether that directory should be cleaned up
after the template has been instantiated.
:raises: `RepositoryNotFound` if a repository directory could not be found.
"""
template = expand_abbreviations(template, abbreviations)

if is_repo_url(template):
if is_zip_file(template):
unzipped_dir = unzip(
zip_uri=template,
is_url=is_repo_url(template),
clone_to_dir=clone_to_dir,
no_input=no_input,
password=password
)
repository_candidates = [unzipped_dir]
cleanup = True
elif is_repo_url(template):
cloned_repo = clone(
repo_url=template,
checkout=checkout,
clone_to_dir=clone_to_dir,
no_input=no_input,
)
repository_candidates = [cloned_repo]
cleanup = False
else:
repository_candidates = [
template,
os.path.join(clone_to_dir, template)
]
cleanup = False

for repo_candidate in repository_candidates:
if repository_has_cookiecutter_json(repo_candidate):
return repo_candidate
return repo_candidate, cleanup

raise RepositoryNotFound(
'A valid repository for "{}" could not be found in the following '
Expand Down
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, delete 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()
80 changes: 28 additions & 52 deletions cookiecutter/vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@
import logging
import os
import subprocess
import sys

from whichcraft import which

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 +23,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 +87,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
Loading
0