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

__get_script_command()   A

Complexity

Conditions 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 14
rs 9.4285
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 json
18
import re
19
import errno
20
21
from jinja2 import Template
22
23
from cookiecutter import utils
24
from .exceptions import FailedHookException
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
    if '_run_hook_in_place' in context and context['_run_hook_in_place']:
66
        script = script_path
67
    else:
68
        script = __create_renderable_hook(script_path, context)
69
70
    try:
71
        result = __do_run_script(script, cwd, json.dumps(context).encode())
72
        json_search = re.findall('(\{.*\})', result[0].decode())
73
74
        return json.loads(json_search[-1]) if json_search else context
75
76
    except ValueError:
77
        return context
78
79
80
def run_hook(hook_name, project_dir, context):
81
    """
82
    Try to find and execute a hook from the specified project directory.
83
84
    :param hook_name: The hook to execute.
85
    :param project_dir: The directory to execute the script from.
86
    :param context: Cookiecutter project context.
87
    """
88
    script = find_hooks().get(hook_name)
89
    if script is None:
90
        logging.debug('No hooks found')
91
        return context
92
93
    return run_script_with_context(script, project_dir, context)
94
95
96
def __create_renderable_hook(script_path, context):
97
    """
98
    Create a renderable hook by copying the real hook and applying the template
99
100
    :param script_path: Absolute path to the base hook.
101
    :param context: Cookiecutter project template context.
102
    """
103
    _, extension = os.path.splitext(script_path)
104
    contents = io.open(script_path, 'r', encoding='utf-8').read()
105
    with tempfile.NamedTemporaryFile(
106
        delete=False,
107
        mode='wb',
108
        suffix=extension
109
    ) as temp:
110
        output = Template(contents).render(**context)
111
        temp.write(output.encode('utf-8'))
112
113
    return temp.name
114
115
116
def __get_script_command(script_path):
117
    """
118
    Get the executable command of a given script
119
120
    :param script_path: Absolute path to the script to run.
121
    """
122
    if script_path.endswith('.py'):
123
        script_command = [sys.executable, script_path]
124
    else:
125
        script_command = [script_path]
126
127
    utils.make_executable(script_path)
128
129
    return script_command
130
131
132
def __do_run_script(script_path, cwd, serialized_context):
133
    """
134
    Executes a script wrinting the given serialized context to its standard
135
    input stream.
136
137
    :param script_path: Absolute path to the script to run.
138
    :param cwd: The directory to run the script from.
139
    :param serialized_context: Serialized Cookiecutter project template
140
                               context.
141
    """
142
    result = (serialized_context, b'')
143
    run_thru_shell = sys.platform.startswith('win')
144
145
    proc = subprocess.Popen(
146
        __get_script_command(script_path),
147
        shell=run_thru_shell,
148
        cwd=cwd,
149
        stdin=subprocess.PIPE,
150
        stdout=subprocess.PIPE,
151
        stderr=subprocess.PIPE
152
    )
153
154
    try:
155
        result = proc.communicate(serialized_context)
156
    except OSError as e:
157
        if e.errno == errno.EINVAL and run_thru_shell:
158
            logging.warn(
159
                'Popen.communicate failed certainly ' +
160
                'because of the issue #19612'
161
            )
162
            pass
163
        else:
164
            raise e
165
166
    exit_status = proc.wait()
167
    if exit_status != EXIT_SUCCESS:
168
        raise FailedHookException(
169
            "Hook script failed (exit status: %d)" % exit_status)
170
171
    return result
172