-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhooks.py
More file actions
204 lines (165 loc) · 6.24 KB
/
hooks.py
File metadata and controls
204 lines (165 loc) · 6.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
"""Functions for discovering and executing various cookiecutter hooks."""
from __future__ import annotations
import errno
import logging
import os
import subprocess
import sys
import tempfile
from typing import TYPE_CHECKING, Any
from jinja2.exceptions import UndefinedError
from cookiecutter import utils
from cookiecutter.exceptions import FailedHookException
from cookiecutter.utils import (
create_env_with_context,
create_tmp_repo_dir,
rmtree,
work_in,
)
if TYPE_CHECKING:
from pathlib import Path
logger = logging.getLogger(__name__)
_HOOKS = [
'pre_prompt',
'pre_gen_project',
'post_gen_project',
]
EXIT_SUCCESS = 0
def valid_hook(hook_file: str, hook_name: str) -> bool:
"""Determine if a hook file is valid.
:param hook_file: The hook file to consider for validity
:param hook_name: The hook to find
:return: The hook file validity
"""
filename = os.path.basename(hook_file)
basename = os.path.splitext(filename)[0]
matching_hook = basename == hook_name
supported_hook = basename in _HOOKS
backup_file = filename.endswith('~')
return matching_hook and supported_hook and not backup_file
def find_hook(hook_name: str, hooks_dir: str = 'hooks') -> list[str] | None:
"""Return a dict of all hook scripts provided.
Must be called with the project template as the current working directory.
Dict's key will be the hook/script's name, without extension, while values
will be the absolute path to the script. Missing scripts will not be
included in the returned dict.
:param hook_name: The hook to find
:param hooks_dir: The hook directory in the template
:return: The absolute path to the hook script or None
"""
logger.debug('hooks_dir is %s', os.path.abspath(hooks_dir))
if not os.path.isdir(hooks_dir):
logger.debug('No hooks/dir in template_dir')
return None
scripts = [
os.path.abspath(os.path.join(hooks_dir, hook_file))
for hook_file in os.listdir(hooks_dir)
if valid_hook(hook_file, hook_name)
]
if len(scripts) == 0:
return None
return scripts
def run_script(script_path: str, cwd: Path | str = '.') -> None:
"""Execute a script from a working directory.
:param script_path: Absolute path to the script to run.
:param cwd: The directory to run the script from.
"""
run_thru_shell = sys.platform.startswith('win')
if script_path.endswith('.py'):
script_command = [sys.executable, script_path]
else:
script_command = [script_path]
utils.make_executable(script_path)
try:
proc = subprocess.Popen(script_command, shell=run_thru_shell, cwd=cwd) # nosec
exit_status = proc.wait()
if exit_status != EXIT_SUCCESS:
raise FailedHookException(
f'Hook script failed (exit status: {exit_status})'
)
except OSError as err:
if err.errno == errno.ENOEXEC:
raise FailedHookException(
'Hook script failed, might be an empty file or missing a shebang'
) from err
raise FailedHookException(f'Hook script failed (error: {err})') from err
def run_script_with_context(
script_path: Path | str, cwd: Path | str, context: dict[str, Any]
) -> None:
"""Execute a script after rendering it with Jinja.
:param script_path: Absolute path to the script to run.
:param cwd: The directory to run the script from.
:param context: Cookiecutter project template context.
"""
_, extension = os.path.splitext(script_path)
with open(script_path, encoding='utf-8') as file:
contents = file.read()
with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp:
env = create_env_with_context(context)
template = env.from_string(contents)
output = template.render(**context)
temp.write(output.encode('utf-8'))
run_script(temp.name, cwd)
def run_hook(hook_name: str, project_dir: Path | str, context: dict[str, Any]) -> None:
"""
Try to find and execute a hook from the specified project directory.
:param hook_name: The hook to execute.
:param project_dir: The directory to execute the script from.
:param context: Cookiecutter project context.
"""
scripts = find_hook(hook_name)
if not scripts:
logger.debug('No %s hook found', hook_name)
return
logger.debug('Running hook %s', hook_name)
for script in scripts:
run_script_with_context(script, project_dir, context)
def run_hook_from_repo_dir(
repo_dir: Path | str,
hook_name: str,
project_dir: Path | str,
context: dict[str, Any],
delete_project_on_failure: bool,
) -> None:
"""Run hook from repo directory, clean project directory if hook fails.
:param repo_dir: Project template input directory.
:param hook_name: The hook to execute.
:param project_dir: The directory to execute the script from.
:param context: Cookiecutter project context.
:param delete_project_on_failure: Delete the project directory on hook
failure?
"""
with work_in(repo_dir):
try:
run_hook(hook_name, project_dir, context)
except (
FailedHookException,
UndefinedError,
):
if delete_project_on_failure:
rmtree(project_dir)
logger.error(
"Stopping generation because %s hook "
"script didn't exit successfully",
hook_name,
)
raise
def run_pre_prompt_hook(repo_dir: Path | str) -> Path | str:
"""Run pre_prompt hook from repo directory.
:param repo_dir: Project template input directory.
"""
# Check if we have a valid pre_prompt script
with work_in(repo_dir):
scripts = find_hook('pre_prompt')
if not scripts:
return repo_dir
# Create a temporary directory
repo_dir = create_tmp_repo_dir(repo_dir)
with work_in(repo_dir):
scripts = find_hook('pre_prompt') or []
for script in scripts:
try:
run_script(script, str(repo_dir))
except FailedHookException as e: # noqa: PERF203
raise FailedHookException('Pre-Prompt Hook script failed') from e
return repo_dir