Skip to content
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

Support template in a subfolder of a repo #1063

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions cookiecutter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,14 @@ def validate_extra_context(ctx, param, value):
u'--debug-file', type=click.Path(), default=None,
help=u'File to be used as a stream for DEBUG logging',
)
@click.option(
u'--subfolder', type=click.Path(), default=None,
help=u'Specify subfolder to search for the template',
)
def main(
template, extra_context, no_input, checkout, verbose,
replay, overwrite_if_exists, output_dir, config_file,
default_config, debug_file):
default_config, debug_file, subfolder):
"""Create a project from a Cookiecutter project template (TEMPLATE).

Cookiecutter is free and open source software, developed and managed by
Expand All @@ -117,7 +121,8 @@ def main(
output_dir=output_dir,
config_file=config_file,
default_config=default_config,
password=os.environ.get('COOKIECUTTER_REPO_PASSWORD')
password=os.environ.get('COOKIECUTTER_REPO_PASSWORD'),
subfolder=subfolder
)
except (OutputDirExistsException,
InvalidModeException,
Expand Down
7 changes: 5 additions & 2 deletions cookiecutter/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
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, password=None):
config_file=None, default_config=False, password=None,
subfolder=None):
"""
Run Cookiecutter just as if using it from the command line.

Expand All @@ -41,6 +42,7 @@ def cookiecutter(
: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.
:param subfolder: Template subfolder inside the repository
"""
if replay and ((no_input is not False) or (extra_context is not None)):
err_msg = (
Expand All @@ -60,7 +62,8 @@ def cookiecutter(
clone_to_dir=config_dict['cookiecutters_dir'],
checkout=checkout,
no_input=no_input,
password=password
password=password,
subfolder=subfolder
)

template_name = os.path.basename(os.path.abspath(repo_dir))
Expand Down
13 changes: 10 additions & 3 deletions cookiecutter/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def repository_has_cookiecutter_json(repo_directory):


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

Expand All @@ -78,6 +78,7 @@ def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
:param checkout: The branch, tag or commit ID to checkout after clone.
:param no_input: Prompt the user at command line for manual configuration?
:param password: The password to use when extracting the repository.
:param subfolder: A subfolder in the repo where the template is taken from.
: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.
Expand Down Expand Up @@ -112,8 +113,14 @@ def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
cleanup = False

for repo_candidate in repository_candidates:
if repository_has_cookiecutter_json(repo_candidate):
return repo_candidate, cleanup
if subfolder:
repo_dir = os.path.normpath(os.path.join(repo_candidate,
subfolder))
else:
repo_dir = repo_candidate

if repository_has_cookiecutter_json(repo_dir):
return repo_dir, cleanup

raise RepositoryNotFound(
'A valid repository for "{}" could not be found in the following '
Expand Down
11 changes: 11 additions & 0 deletions tests/fake-repo-tmpl-sf/fake_template/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"full_name": "Audrey Roy",
"email": "audreyr@gmail.com",
"github_username": "audreyr",
"project_name": "Fake Project Templated",
"repo_name": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}",
"project_short_description": "This is a fake project.",
"release_date": "2013-07-28",
"year": "2013",
"version": "0.1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
============
Fake Project
============

Blah!!!!
46 changes: 46 additions & 0 deletions tests/repository/test_determine_repo_dir_uses_subfolder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
import os

import pytest

from cookiecutter import repository


@pytest.fixture
def template_url():
"""URL to example Cookiecutter template on GitHub.

Note: when used, git clone is mocked.
"""
return 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git'


@pytest.fixture
def subfolder():
"""Returns normalized subfolder path"""
return os.path.normpath('tests/fake-repo-tmpl-sf/fake_template')


def test_subfolder_added_to_repo_dir(
mocker, template_url, user_config_data):
"""`determine_repo_dir()` returns cloned repo path
with subfolder attached.
"""

mocker.patch(
'cookiecutter.repository.clone',
return_value='tests/fake-repo-tmpl-sf',
autospec=True
)

project_dir, cleanup = repository.determine_repo_dir(
template_url,
abbreviations={},
clone_to_dir=user_config_data['cookiecutters_dir'],
checkout=None,
no_input=True,
subfolder='fake_template'
)

assert os.path.isdir(project_dir)
assert subfolder() == project_dir
36 changes: 36 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def test_cli_replay(mocker, cli_runner):
config_file=None,
default_config=False,
extra_context=None,
subfolder=None,
password=None
)

Expand Down Expand Up @@ -131,6 +132,7 @@ def test_cli_exit_on_noinput_and_replay(mocker, cli_runner):
default_config=False,
extra_context=None,
password=None,
subfolder=None
)


Expand Down Expand Up @@ -163,6 +165,7 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay(
default_config=False,
extra_context=None,
password=None,
subfolder=None
)


Expand Down Expand Up @@ -223,6 +226,7 @@ def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir):
default_config=False,
extra_context=None,
password=None,
subfolder=None
)


Expand Down Expand Up @@ -262,6 +266,7 @@ def test_user_config(mocker, cli_runner, user_config_path):
default_config=False,
extra_context=None,
password=None,
subfolder=None
)


Expand Down Expand Up @@ -290,6 +295,7 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path):
default_config=True,
extra_context=None,
password=None,
subfolder=None
)


Expand All @@ -313,6 +319,7 @@ def test_default_user_config(mocker, cli_runner):
default_config=True,
extra_context=None,
password=None,
subfolder=None
)


Expand Down Expand Up @@ -439,3 +446,32 @@ def test_debug_file_verbose(cli_runner, debug_file):
)
assert context_log in debug_file.readlines(cr=False)
assert context_log in result.output


def test_cli_subfolder(mocker, cli_runner, output_dir_flag, output_dir):
mock_cookiecutter = mocker.patch(
'cookiecutter.cli.cookiecutter'
)

template_path = 'tests/fake-repo-pre/'
subfolder = 'fake-repo-subfolder'
result = cli_runner('--subfolder',
subfolder,
template_path,
output_dir_flag,
output_dir)

assert result.exit_code == 0
mock_cookiecutter.assert_called_once_with(
template_path,
None,
False,
replay=False,
overwrite_if_exists=False,
output_dir=output_dir,
config_file=None,
default_config=False,
extra_context=None,
password=None,
subfolder=subfolder
)