@@ -20,6 +20,7 @@
Loading
20 20
)
21 21
from cookiecutter.find import find_template
22 22
from cookiecutter.hooks import run_hook
23 +
from cookiecutter.prompt import prompt_for_config
23 24
from cookiecutter.utils import make_sure_path_exists, rmtree, work_in
24 25
25 26
logger = logging.getLogger(__name__)
@@ -67,7 +68,7 @@
Loading
67 68
            context[variable] = overwrite
68 69
69 70
70 -
def generate_context(
71 +
def merge_contexts(
71 72
    context_file='cookiecutter.json', default_context=None, extra_context=None
72 73
):
73 74
    """Generate the context for a Cookiecutter project template.
@@ -111,6 +112,39 @@
Loading
111 112
    return context
112 113
113 114
115 +
def _run_hook_from_repo_dir(repo_dir, hook_name, context, hook_cwd=None, isolate=True):
116 +
    hook_cwd = hook_cwd or repo_dir
117 +
    with work_in(repo_dir):
118 +
        try:
119 +
            run_hook(hook_name, hook_cwd, context, isolate)
120 +
        except FailedHookException:
121 +
            logger.error(
122 +
                "Stopping generation because %s hook "
123 +
                "script didn't exit successfully",
124 +
                hook_name,
125 +
            )
126 +
            raise
127 +
128 +
129 +
def generate_context(repo_dir, config_dict, extra_context=None, no_input=False):
130 +
    context_file = os.path.join(repo_dir, 'cookiecutter.json')
131 +
    logger.debug('context_file is %s', context_file)
132 +
    context = merge_contexts(
133 +
        context_file=context_file,
134 +
        default_context=config_dict['default_context'],
135 +
        extra_context=extra_context,
136 +
    )
137 +
    _run_hook_from_repo_dir(repo_dir, 'pre_context_prompt', context, isolate=False)
138 +
139 +
    # prompt the user to manually configure at the command line.
140 +
    # except when 'no-input' flag is set
141 +
    context['cookiecutter'] = prompt_for_config(context, no_input)
142 +
143 +
    _run_hook_from_repo_dir(repo_dir, 'post_context_prompt', context, isolate=False)
144 +
145 +
    return context
146 +
147 +
114 148
def generate_file(project_dir, infile, context, env, skip_if_file_exists=False):
115 149
    """Render filename of infile as name of outfile, handle infile correctly.
116 150
@@ -226,8 +260,8 @@
Loading
226 260
        raise NonTemplatedInputDirException
227 261
228 262
229 -
def _run_hook_from_repo_dir(
230 -
    repo_dir, hook_name, project_dir, context, delete_project_on_failure
263 +
def _run_hook_for_project(
264 +
        repo_dir, hook_name, project_dir, context, delete_project_on_failure
231 265
):
232 266
    """Run hook from repo directory, clean project directory if hook fails.
233 267
@@ -238,18 +272,12 @@
Loading
238 272
    :param delete_project_on_failure: Delete the project directory on hook
239 273
        failure?
240 274
    """
241 -
    with work_in(repo_dir):
242 -
        try:
243 -
            run_hook(hook_name, project_dir, context)
244 -
        except FailedHookException:
245 -
            if delete_project_on_failure:
246 -
                rmtree(project_dir)
247 -
            logger.error(
248 -
                "Stopping generation because %s hook "
249 -
                "script didn't exit successfully",
250 -
                hook_name,
251 -
            )
252 -
            raise
275 +
    try:
276 +
        _run_hook_from_repo_dir(repo_dir, hook_name, context, hook_cwd=project_dir)
277 +
    except FailedHookException:
278 +
        if delete_project_on_failure:
279 +
            rmtree(project_dir)
280 +
        raise
253 281
254 282
255 283
def generate_files(
@@ -301,7 +329,7 @@
Loading
301 329
    delete_project_on_failure = output_directory_created
302 330
303 331
    if accept_hooks:
304 -
        _run_hook_from_repo_dir(
332 +
        _run_hook_for_project(
305 333
            repo_dir, 'pre_gen_project', project_dir, context, delete_project_on_failure
306 334
        )
307 335
@@ -371,7 +399,7 @@
Loading
371 399
                    raise UndefinedVariableInTemplate(msg, err, context)
372 400
373 401
    if accept_hooks:
374 -
        _run_hook_from_repo_dir(
402 +
        _run_hook_for_project(
375 403
            repo_dir,
376 404
            'post_gen_project',
377 405
            project_dir,

@@ -15,6 +15,8 @@
Loading
15 15
_HOOKS = [
16 16
    'pre_gen_project',
17 17
    'post_gen_project',
18 +
    'pre_context_prompt',
19 +
    'post_context_prompt',
18 20
]
19 21
EXIT_SUCCESS = 0
20 22
@@ -64,7 +66,7 @@
Loading
64 66
    return scripts
65 67
66 68
67 -
def run_script(script_path, cwd='.'):
69 +
def run_script_in_subprocess(script_path, cwd='.'):
68 70
    """Execute a script from a working directory.
69 71
70 72
    :param script_path: Absolute path to the script to run.
@@ -93,12 +95,35 @@
Loading
93 95
        raise FailedHookException('Hook script failed (error: {})'.format(os_error))
94 96
95 97
96 -
def run_script_with_context(script_path, cwd, context):
98 +
def run_scrip_in_current_process(script_path, cwd='.', context=None):
99 +
    """Execute a script in the same process, which make it possible to mutate some
100 +
    objects, like context.
101 +
102 +
    :param script_path: Absolute path to the script to run.
103 +
    :param cwd: The directory to run the script from.
104 +
    :param context: Cookiecutter project template context.
105 +
    """
106 +
    with open(script_path) as f:
107 +
        code = f.read()
108 +
109 +
    ns = {
110 +
        'context': context,
111 +
    }
112 +
113 +
    try:
114 +
        with utils.work_in(cwd):
115 +
            exec(code, ns)
116 +
    except Exception as error:
117 +
        raise FailedHookException('Hook script failed (error: {})'.format(error))
118 +
119 +
120 +
def run_script_with_context(script_path, cwd, context, isolate=True):
97 121
    """Execute a script after rendering it with Jinja.
98 122
99 123
    :param script_path: Absolute path to the script to run.
100 124
    :param cwd: The directory to run the script from.
101 125
    :param context: Cookiecutter project template context.
126 +
    :param isolate: If True, run script in isolated environment (subprocess).
102 127
    """
103 128
    _, extension = os.path.splitext(script_path)
104 129
@@ -111,16 +136,20 @@
Loading
111 136
        output = template.render(**context)
112 137
        temp.write(output.encode('utf-8'))
113 138
114 -
    run_script(temp.name, cwd)
139 +
    if isolate:
140 +
        run_script_in_subprocess(temp.name, cwd)
141 +
    else:
142 +
        run_scrip_in_current_process(temp.name, cwd, context)
115 143
116 144
117 -
def run_hook(hook_name, project_dir, context):
145 +
def run_hook(hook_name, project_dir, context, isolate=True):
118 146
    """
119 147
    Try to find and execute a hook from the specified project directory.
120 148
121 149
    :param hook_name: The hook to execute.
122 150
    :param project_dir: The directory to execute the script from.
123 151
    :param context: Cookiecutter project context.
152 +
    :param isolate: If True, run script in isolated environment (subprocess).
124 153
    """
125 154
    scripts = find_hook(hook_name)
126 155
    if not scripts:
@@ -128,4 +157,4 @@
Loading
128 157
        return
129 158
    logger.debug('Running hook %s', hook_name)
130 159
    for script in scripts:
131 -
        run_script_with_context(script, project_dir, context)
160 +
        run_script_with_context(script, project_dir, context, isolate)

@@ -9,8 +9,7 @@
Loading
9 9
10 10
from cookiecutter.config import get_user_config
11 11
from cookiecutter.exceptions import InvalidModeException
12 -
from cookiecutter.generate import generate_context, generate_files
13 -
from cookiecutter.prompt import prompt_for_config
12 +
from cookiecutter.generate import generate_files, generate_context
14 13
from cookiecutter.replay import dump, load
15 14
from cookiecutter.repository import determine_repo_dir
16 15
from cookiecutter.utils import rmtree
@@ -82,18 +81,7 @@
Loading
82 81
            path, template_name = os.path.split(os.path.splitext(replay)[0])
83 82
            context = load(path, template_name)
84 83
    else:
85 -
        context_file = os.path.join(repo_dir, 'cookiecutter.json')
86 -
        logger.debug('context_file is %s', context_file)
87 -
88 -
        context = generate_context(
89 -
            context_file=context_file,
90 -
            default_context=config_dict['default_context'],
91 -
            extra_context=extra_context,
92 -
        )
93 -
94 -
        # prompt the user to manually configure at the command line.
95 -
        # except when 'no-input' flag is set
96 -
        context['cookiecutter'] = prompt_for_config(context, no_input)
84 +
        context = generate_context(repo_dir, config_dict, extra_context, no_input)
97 85
98 86
        # include template dir or url in the context dict
99 87
        context['cookiecutter']['_template'] = template
@@ -118,3 +106,5 @@
Loading
118 106
        rmtree(repo_dir)
119 107
120 108
    return result
109 +
110 +
Files Coverage
cookiecutter 98.94%
__main__.py 100.00%
Project Totals (19 files) 98.94%
2983.3
TRAVIS_PYTHON_VERSION=3.7
TRAVIS_OS_NAME=linux
TOXENV=py37
2983.4
TRAVIS_PYTHON_VERSION=3.8
TRAVIS_OS_NAME=linux
TOXENV=py38
2983.2
TRAVIS_PYTHON_VERSION=3.6
TRAVIS_OS_NAME=linux
TOXENV=py36
2983.6
TRAVIS_OS_NAME=windows
TOXENV=py37
2983.5
TRAVIS_OS_NAME=windows
TOXENV=py36
2983.7
TRAVIS_OS_NAME=windows
TOXENV=py38
1
# comment spam as user can always click the failed coverage check
2
comment: false
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading