Completed
Pull Request — master (#694)
by Eric
02:14
created

__do_run_script()   B

Complexity

Conditions 5

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

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