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

run_script_with_context()   B

Complexity

Conditions 4

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 28
rs 8.5806
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(script_path, cwd='.', context={}):
56
    """
57
    Executes a script from a working directory.
58
59
    :param script_path: Absolute path to the script to run.
60
    :param cwd: The directory to run the script from.
61
    """
62
    run_thru_shell = sys.platform.startswith('win')
63
    if script_path.endswith('.py'):
64
        script_command = [sys.executable, script_path]
65
    else:
66
        script_command = [script_path]
67
68
    utils.make_executable(script_path)
69
70
    proc = subprocess.Popen(
71
        script_command,
72
        shell=run_thru_shell,
73
        cwd=cwd,
74
        stdin=subprocess.PIPE,
75
        stdout=subprocess.PIPE,
76
        stderr=subprocess.PIPE
77
    )
78
    result = proc.communicate(json.dumps(context).encode())
79
80
    exit_status = proc.wait()
81
    if exit_status != EXIT_SUCCESS:
82
        raise FailedHookException(
83
            "Hook script failed (exit status: %d)" % exit_status)
84
    try:
85
        json_search = re.findall('(\{.*\})', result[0].decode())
86
        return json.loads(json_search[-1]) if json_search else context
87
    except ValueError:
88
        return context
89
90
91
def run_script_with_context(script_path, cwd, context):
92
    """
93
    Executes a script after rendering with it Jinja.
94
95
    :param script_path: Absolute path to the script to run.
96
    :param cwd: The directory to run the script from.
97
    :param context: Cookiecutter project template context.
98
    """
99
    if '_no_hookcopy' in context and re.match(
100
        r'yes',
101
        context['_no_hookcopy'],
102
        re.I
103
    ):
104
        return run_script(script_path, cwd, context)
105
106
    _, extension = os.path.splitext(script_path)
107
108
    contents = io.open(script_path, 'r', encoding='utf-8').read()
109
110
    with tempfile.NamedTemporaryFile(
111
        delete=False,
112
        mode='wb',
113
        suffix=extension
114
    ) as temp:
115
        output = Template(contents).render(**context)
116
        temp.write(output.encode('utf-8'))
117
118
    return run_script(temp.name, cwd, context)
119
120
121
def run_hook(hook_name, project_dir, context):
122
    """
123
    Try to find and execute a hook from the specified project directory.
124
125
    :param hook_name: The hook to execute.
126
    :param project_dir: The directory to execute the script from.
127
    :param context: Cookiecutter project context.
128
    """
129
    script = find_hooks().get(hook_name)
130
    if script is None:
131
        logging.debug('No hooks found')
132
        return context
133
    return run_script_with_context(script, project_dir, context)
134