Completed
Pull Request — master (#694)
by Eric
01:20
created

run_script_with_context()   F

Complexity

Conditions 9

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 1
Metric Value
cc 9
c 3
b 1
f 1
dl 0
loc 44
rs 3
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
4
"""
5
cookiecutter.hooks
6
------------------
7
8
Functions for discovering and executing various cookiecutter hooks.
9
"""
10
11
import io
12
import logging
13
import os
14
import subprocess
15
import sys
16
import tempfile
17
import errno
18
19
from jinja2 import Template
20
21
from pydoc import locate
22
from cookiecutter import utils
23
from .exceptions import FailedHookException, BadSerializedStringFormat
24
from .serialization import SerializationFacade, make_persistent
25
26
27
_HOOKS = [
28
    'pre_gen_project',
29
    'post_gen_project',
30
    # TODO: other hooks should be listed here
31
]
32
EXIT_SUCCESS = 0
33
34
35
def find_hooks():
36
    """
37
    Must be called with the project template as the current working directory.
38
    Returns a dict of all hook scripts provided.
39
    Dict's key will be the hook/script's name, without extension, while
40
    values will be the absolute path to the script.
41
    Missing scripts will not be included in the returned dict.
42
    """
43
    hooks_dir = 'hooks'
44
    r = {}
45
    logging.debug('hooks_dir is {0}'.format(hooks_dir))
46
    if not os.path.isdir(hooks_dir):
47
        logging.debug('No hooks/ dir in template_dir')
48
        return r
49
    for f in os.listdir(hooks_dir):
50
        basename = os.path.splitext(os.path.basename(f))[0]
51
        if basename in _HOOKS:
52
            r[basename] = os.path.abspath(os.path.join(hooks_dir, f))
53
    return r
54
55
56
def run_script_with_context(script_path, cwd, context):
57
    """
58
    Executes a script either after rendering with it Jinja or in place without
59
    template rendering.
60
61
    :param script_path: Absolute path to the script to run.
62
    :param cwd: The directory to run the script from.
63
    :param context: Cookiecutter project template context.
64
    """
65
    lazy_load_from_extra_dir(os.path.dirname(os.path.dirname(script_path)))
66
67
    # TODO: refactor the following as it is terrific
68
    current_context = context[
69
        'cookiecutter'] if 'cookiecutter' in context else context
70
71
    if ('_run_hook_in_place' in current_context and
72
            current_context['_run_hook_in_place']):
73
        script = script_path
74
    else:
75
        script = __create_renderable_hook(script_path, context)
76
77
    try:
78
        # TODO: refactor the following ugly part
79
        serializers = {}
80
        usetype = 'json'
81
        if '_serializers' in current_context and 'classes' \
82
                in current_context['_serializers']:
83
            classes = current_context['_serializers']['classes']
84
            for type in classes:
85
                serializers[type] = locate(classes[type], 1)
86
87
            if 'use' in current_context['_serializers']:
88
                usetype = current_context['_serializers']['use']
89
90
        serializer = SerializationFacade(serializers).use(usetype)
91
        make_persistent(serializer)
92
93
        result = __do_run_script(
94
            script, cwd, serializer.serialize(context).encode())
95
96
        return serializer.deserialize(result[0].decode())
97
98
    except BadSerializedStringFormat:
99
        return context
100
101
102
def run_hook(hook_name, project_dir, context):
103
    """
104
    Try to find and execute a hook from the specified project directory.
105
106
    :param hook_name: The hook to execute.
107
    :param project_dir: The directory to execute the script from.
108
    :param context: Cookiecutter project context.
109
    """
110
    script = find_hooks().get(hook_name)
111
    if script is None:
112
        logging.debug('No hooks found')
113
        return context
114
115
    return run_script_with_context(script, project_dir, context)
116
117
118
def lazy_load_from_extra_dir(template_dir):
119
    """
120
    permit lazy load from the 'extra' directory
121
    :param template_dir: the project template directory
122
    """
123
    extra_dir = os.path.abspath(os.path.join(template_dir, 'extra'))
124
    if os.path.exists(extra_dir) and extra_dir not in sys.path:
125
        sys.path.insert(1, extra_dir)
126
127
128
def __create_renderable_hook(script_path, context):
129
    """
130
    Create a renderable hook by copying the real hook and applying the template
131
132
    :param script_path: Absolute path to the base hook.
133
    :param context: Cookiecutter project template context.
134
    """
135
    _, extension = os.path.splitext(script_path)
136
    contents = io.open(script_path, 'r', encoding='utf-8').read()
137
    with tempfile.NamedTemporaryFile(
138
        delete=False,
139
        mode='wb',
140
        suffix=extension
141
    ) as temp:
142
        output = Template(contents).render(**context)
143
        temp.write(output.encode('utf-8'))
144
145
    return temp.name
146
147
148
def __get_script_command(script_path):
149
    """
150
    Get the executable command of a given script
151
152
    :param script_path: Absolute path to the script to run.
153
    """
154
    if script_path.endswith('.py'):
155
        script_command = [sys.executable, script_path]
156
    else:
157
        script_command = [script_path]
158
159
    utils.make_executable(script_path)
160
161
    return script_command
162
163
164
def __do_run_script(script_path, cwd, serialized_context):
165
    """
166
    Executes a script wrinting the given serialized context to its standard
167
    input stream.
168
169
    :param script_path: Absolute path to the script to run.
170
    :param cwd: The directory to run the script from.
171
    :param serialized_context: Serialized Cookiecutter project template
172
                               context.
173
    """
174
    result = (serialized_context, b'')
175
    run_thru_shell = sys.platform.startswith('win')
176
177
    os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
178
179
    proc = subprocess.Popen(
180
        __get_script_command(script_path),
181
        shell=run_thru_shell,
182
        cwd=cwd,
183
        stdin=subprocess.PIPE,
184
        stdout=subprocess.PIPE,
185
        stderr=subprocess.PIPE
186
    )
187
188
    try:
189
        result = proc.communicate(serialized_context)
190
    except OSError as e:
191
        if e.errno == errno.EINVAL and run_thru_shell:
192
            logging.warn(
193
                'Popen.communicate failed certainly ' +
194
                'because of the issue #19612'
195
            )
196
            pass
197
        else:
198
            raise e
199
200
    exit_status = proc.wait()
201
    if exit_status != EXIT_SUCCESS:
202
        raise FailedHookException(
203
            "Hook script failed (exit status: %d): \n%s" %
204
            (exit_status, result[1]))
205
206
    return result
207