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

__do_run_script()   B

Complexity

Conditions 2

Size

Total Lines 28

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 28
rs 8.8571
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
20
from jinja2 import Template
21
22
from cookiecutter import utils
23
from .exceptions import FailedHookException
24
25
26
_HOOKS = [
27
    'pre_gen_project',
28
    'post_gen_project',
29
    # TODO: other hooks should be listed here
30
]
31
EXIT_SUCCESS = 0
32
33
34
def find_hooks():
35
    """
36
    Must be called with the project template as the current working directory.
37
    Returns a dict of all hook scripts provided.
38
    Dict's key will be the hook/script's name, without extension, while
39
    values will be the absolute path to the script.
40
    Missing scripts will not be included in the returned dict.
41
    """
42
    hooks_dir = 'hooks'
43
    r = {}
44
    logging.debug('hooks_dir is {0}'.format(hooks_dir))
45
    if not os.path.isdir(hooks_dir):
46
        logging.debug('No hooks/ dir in template_dir')
47
        return r
48
    for f in os.listdir(hooks_dir):
49
        basename = os.path.splitext(os.path.basename(f))[0]
50
        if basename in _HOOKS:
51
            r[basename] = os.path.abspath(os.path.join(hooks_dir, f))
52
    return r
53
54
55
def run_script_with_context(script_path, cwd, context):
56
    """
57
    Executes a script either after rendering with it Jinja or in place without
58
    template rendering.
59
60
    :param script_path: Absolute path to the script to run.
61
    :param cwd: The directory to run the script from.
62
    :param context: Cookiecutter project template context.
63
    """
64
    if '_run_hook_in_place' in context and context['_run_hook_in_place']:
65
        script = script_path
66
    else:
67
        script = __create_renderable_hook(script_path, context)
68
69
    try:
70
        result = __do_run_script(script, cwd, json.dumps(context).encode())
71
        json_search = re.findall('(\{.*\})', result[0].decode())
72
        return json.loads(json_search[-1]) if json_search else context
73
    except ValueError:
74
        return context
75
76
77
def run_hook(hook_name, project_dir, context):
78
    """
79
    Try to find and execute a hook from the specified project directory.
80
81
    :param hook_name: The hook to execute.
82
    :param project_dir: The directory to execute the script from.
83
    :param context: Cookiecutter project context.
84
    """
85
    script = find_hooks().get(hook_name)
86
    if script is None:
87
        logging.debug('No hooks found')
88
        return context
89
    return run_script_with_context(script, project_dir, context)
90
91
92
def __create_renderable_hook(script_path, context):
93
    """
94
    Create a renderable hook by copying the real hook and applying the template
95
96
    :param script_path: Absolute path to the base hook.
97
    :param context: Cookiecutter project template context.
98
    """
99
    _, extension = os.path.splitext(script_path)
100
    contents = io.open(script_path, 'r', encoding='utf-8').read()
101
    with tempfile.NamedTemporaryFile(
102
        delete=False,
103
        mode='wb',
104
        suffix=extension
105
    ) as temp:
106
        output = Template(contents).render(**context)
107
        temp.write(output.encode('utf-8'))
108
    return temp.name
109
110
111
def __get_script_command(script_path):
112
    """
113
    Get the executable command of a given script
114
115
    :param script_path: Absolute path to the script to run.
116
    """
117
    if script_path.endswith('.py'):
118
        script_command = [sys.executable, script_path]
119
    else:
120
        script_command = [script_path]
121
122
    utils.make_executable(script_path)
123
124
    return script_command
125
126
127
def __do_run_script(script_path, cwd, serialized_context):
128
    """
129
    Executes a script wrinting the given serialized context to its standard
130
    input stream.
131
132
    :param script_path: Absolute path to the script to run.
133
    :param cwd: The directory to run the script from.
134
    :param serialized_context: Serialized Cookiecutter project template
135
                               context.
136
    """
137
    run_thru_shell = sys.platform.startswith('win')
138
139
    proc = subprocess.Popen(
140
        __get_script_command(script_path),
141
        shell=run_thru_shell,
142
        cwd=cwd,
143
        stdin=subprocess.PIPE,
144
        stdout=subprocess.PIPE,
145
        stderr=subprocess.PIPE
146
    )
147
    result = proc.communicate(serialized_context)
148
149
    exit_status = proc.wait()
150
    if exit_status != EXIT_SUCCESS:
151
        raise FailedHookException(
152
            "Hook script failed (exit status: %d)" % exit_status)
153
154
    return result
155