cookiecutter.hooks.valid_hook()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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