Completed
Pull Request — master (#980)
by
unknown
43s
created

get_hooks_env()   A

Complexity

Conditions 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 18
rs 9.4285
1
# -*- coding: utf-8 -*-
2
3
"""Functions for discovering and executing various cookiecutter hooks."""
4
5
import errno
6
import io
7
import logging
8
import os
9
import subprocess
10
import sys
11
import tempfile
12
13
from cookiecutter import utils
14
from cookiecutter.environment import StrictEnvironment
15
from .exceptions import FailedHookException
16
17
logger = logging.getLogger(__name__)
18
19
_HOOKS = [
20
    'pre_gen_project',
21
    'post_gen_project',
22
]
23
EXIT_SUCCESS = 0
24
25
26
def valid_hook(hook_file, hook_name):
27
    """Determine if a hook file is valid.
28
29
    :param hook_file: The hook file to consider for validity
30
    :param hook_name: The hook to find
31
    :return: The hook file validity
32
    """
33
    filename = os.path.basename(hook_file)
34
    basename = os.path.splitext(filename)[0]
35
36
    matching_hook = basename == hook_name
37
    supported_hook = basename in _HOOKS
38
    backup_file = filename.endswith('~')
39
40
    return matching_hook and supported_hook and not backup_file
41
42
43
def find_hook(hook_name, hooks_dir='hooks'):
44
    """Return a dict of all hook scripts provided.
45
46
    Must be called with the project template as the current working directory.
47
    Dict's key will be the hook/script's name, without extension, while values
48
    will be the absolute path to the script. Missing scripts will not be
49
    included in the returned dict.
50
51
    :param hook_name: The hook to find
52
    :param hooks_dir: The hook directory in the template
53
    :return: The absolute path to the hook script or None
54
    """
55
    logger.debug('hooks_dir is {}'.format(os.path.abspath(hooks_dir)))
56
57
    if not os.path.isdir(hooks_dir):
58
        logger.debug('No hooks/ dir in template_dir')
59
        return None
60
61
    for hook_file in os.listdir(hooks_dir):
62
        if valid_hook(hook_file, hook_name):
63
            return os.path.abspath(os.path.join(hooks_dir, hook_file))
64
65
    return None
66
67
68
def run_script(script_path, cwd='.'):
69
    """Execute a script from a working directory.
70
71
    :param script_path: Absolute path to the script to run.
72
    :param cwd: The directory to run the script from.
73
    """
74
    run_thru_shell = sys.platform.startswith('win')
75
    if script_path.endswith('.py'):
76
        script_command = [sys.executable, script_path]
77
    else:
78
        script_command = [script_path]
79
80
    utils.make_executable(script_path)
81
82
    try:
83
        proc = subprocess.Popen(
84
            script_command,
85
            env=get_hooks_env(cwd),
86
            shell=run_thru_shell,
87
            cwd=cwd
88
        )
89
        exit_status = proc.wait()
90
        if exit_status != EXIT_SUCCESS:
91
            raise FailedHookException(
92
                'Hook script failed (exit status: {})'.format(exit_status)
93
            )
94
    except OSError as os_error:
95
        if os_error.errno == errno.ENOEXEC:
96
            raise FailedHookException(
97
                'Hook script failed, might be an '
98
                'empty file or missing a shebang'
99
            )
100
        raise FailedHookException(
101
            'Hook script failed (error: {})'.format(os_error)
102
        )
103
104
105
def run_script_with_context(script_path, cwd, context):
106
    """Execute a script after rendering it with Jinja.
107
108
    :param script_path: Absolute path to the script to run.
109
    :param cwd: The directory to run the script from.
110
    :param context: Cookiecutter project template context.
111
    """
112
    _, extension = os.path.splitext(script_path)
113
114
    contents = io.open(script_path, 'r', encoding='utf-8').read()
115
116
    with tempfile.NamedTemporaryFile(
117
        delete=False,
118
        mode='wb',
119
        suffix=extension
120
    ) as temp:
121
        env = StrictEnvironment(
122
            context=context,
123
            keep_trailing_newline=True,
124
        )
125
        template = env.from_string(contents)
126
        output = template.render(**context)
127
        temp.write(output.encode('utf-8'))
128
129
    run_script(temp.name, cwd)
130
131
132
def run_hook(hook_name, project_dir, context):
133
    """
134
    Try to find and execute a hook from the specified project directory.
135
136
    :param hook_name: The hook to execute.
137
    :param project_dir: The directory to execute the script from.
138
    :param context: Cookiecutter project context.
139
    """
140
    script = find_hook(hook_name)
141
    if script is None:
142
        logger.debug('No {} hook found'.format(hook_name))
143
        return
144
    logger.debug('Running hook {}'.format(hook_name))
145
    run_script_with_context(script, project_dir, context)
146
147
148
def get_hooks_env(cwd='.', hooks_dir='hooks'):
149
    """
150
    Create and return a copy of the environmental variables including
151
    the current hooks path appended to the PYTHONPATH. This is used for
152
    Popen to enable local imports when running hooks.
153
154
    :param cwd: The directory to run the script from.
155
    :param hooks_dir: The hook directory in the template.
156
    :return: A copy of the environmental variables with the addition of
157
    the current hooks path appended to the PYTHONPATH.
158
    """
159
    hooks_env = os.environ.copy()
160
161
    PYTHONPATH = hooks_env.get("PYTHONPATH", "")
162
    hooks_env["PYTHONPATH"] = \
163
        os.path.join(os.path.dirname(cwd), hooks_dir) + ":" + PYTHONPATH
164
165
    return hooks_env
166