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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
"""Module containing the main git hook interface and helpers.
.. autofunction:: hook
.. autofunction:: install
"""
import contextlib
import os
import os.path
import shutil
import stat
import subprocess
import sys
import tempfile
from flake8 import defaults
from flake8 import exceptions
__all__ = ("hook", "install")
def hook(lazy=False, strict=False):
"""Execute Flake8 on the files in git's index.
Determine which files are about to be committed and run Flake8 over them
to check for violations.
:param bool lazy:
Find files not added to the index prior to committing. This is useful
if you frequently use ``git commit -a`` for example. This defaults to
False since it will otherwise include files not in the index.
:param bool strict:
If True, return the total number of errors/violations found by Flake8.
This will cause the hook to fail.
:returns:
Total number of errors found during the run.
:rtype:
int
"""
# NOTE(sigmavirus24): Delay import of application until we need it.
from flake8.main import application
app = application.Application()
with make_temporary_directory() as tempdir:
filepaths = list(copy_indexed_files_to(tempdir, lazy))
app.initialize(["."])
app.options.exclude = update_excludes(app.options.exclude, tempdir)
app.options._running_from_vcs = True
# Apparently there are times when there are no files to check (e.g.,
# when amending a commit). In those cases, let's not try to run checks
# against nothing.
if filepaths:
app.run_checks(filepaths)
# If there were files to check, update their paths and report the errors
if filepaths:
update_paths(app.file_checker_manager, tempdir)
app.report_errors()
if strict:
return app.result_count
return 0
def install():
"""Install the git hook script.
This searches for the ``.git`` directory and will install an executable
pre-commit python script in the hooks sub-directory if one does not
already exist.
It will also print a message to stdout about how to configure the hook.
:returns:
True if successful, False if the git directory doesn't exist.
:rtype:
bool
:raises:
flake8.exceptions.GitHookAlreadyExists
"""
git_directory = find_git_directory()
if git_directory is None or not os.path.exists(git_directory):
return False
hooks_directory = os.path.join(git_directory, "hooks")
if not os.path.exists(hooks_directory):
os.mkdir(hooks_directory)
pre_commit_file = os.path.abspath(
os.path.join(hooks_directory, "pre-commit")
)
if os.path.exists(pre_commit_file):
raise exceptions.GitHookAlreadyExists(
"File already exists", path=pre_commit_file
)
executable = get_executable()
with open(pre_commit_file, "w") as fd:
fd.write(_HOOK_TEMPLATE.format(executable=executable))
# NOTE(sigmavirus24): The following sets:
# - read, write, and execute permissions for the owner
# - read permissions for people in the group
# - read permissions for other people
# The owner needs the file to be readable, writable, and executable
# so that git can actually execute it as a hook.
pre_commit_permissions = stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH
os.chmod(pre_commit_file, pre_commit_permissions)
print("git pre-commit hook installed, for configuration options see")
print("http://flake8.pycqa.org/en/latest/user/using-hooks.html")
return True
def get_executable():
if sys.executable is not None:
return sys.executable
return "/usr/bin/env python"
def find_git_directory():
rev_parse = piped_process(["git", "rev-parse", "--git-dir"])
(stdout, _) = rev_parse.communicate()
stdout = to_text(stdout)
if rev_parse.returncode == 0:
return stdout.strip()
return None
def copy_indexed_files_to(temporary_directory, lazy):
# some plugins (e.g. flake8-isort) need these files to run their checks
setup_cfgs = find_setup_cfgs(lazy)
for filename in setup_cfgs:
contents = get_staged_contents_from(filename)
copy_file_to(temporary_directory, filename, contents)
modified_files = find_modified_files(lazy)
for filename in modified_files:
contents = get_staged_contents_from(filename)
yield copy_file_to(temporary_directory, filename, contents)
def copy_file_to(destination_directory, filepath, contents):
directory, filename = os.path.split(os.path.abspath(filepath))
temporary_directory = make_temporary_directory_from(
destination_directory, directory
)
if not os.path.exists(temporary_directory):
os.makedirs(temporary_directory)
temporary_filepath = os.path.join(temporary_directory, filename)
with open(temporary_filepath, "wb") as fd:
fd.write(contents)
return temporary_filepath
def make_temporary_directory_from(destination, directory):
prefix = os.path.commonprefix([directory, destination])
common_directory_path = os.path.relpath(directory, start=prefix)
return os.path.join(destination, common_directory_path)
def find_modified_files(lazy):
diff_index_cmd = [
"git",
"diff-index",
"--cached",
"--name-only",
"--diff-filter=ACMRTUXB",
"HEAD",
]
if lazy:
diff_index_cmd.remove("--cached")
diff_index = piped_process(diff_index_cmd)
(stdout, _) = diff_index.communicate()
stdout = to_text(stdout)
return stdout.splitlines()
def find_setup_cfgs(lazy):
setup_cfg_cmd = ["git", "ls-files", "--cached", "*setup.cfg"]
if lazy:
setup_cfg_cmd.remove("--cached")
extra_files = piped_process(setup_cfg_cmd)
(stdout, _) = extra_files.communicate()
stdout = to_text(stdout)
return stdout.splitlines()
def get_staged_contents_from(filename):
git_show = piped_process(["git", "show", ":{0}".format(filename)])
(stdout, _) = git_show.communicate()
return stdout
@contextlib.contextmanager
def make_temporary_directory():
temporary_directory = tempfile.mkdtemp()
yield temporary_directory
shutil.rmtree(temporary_directory, ignore_errors=True)
def to_text(string):
"""Ensure that the string is text."""
if callable(getattr(string, "decode", None)):
return string.decode("utf-8")
return string
def piped_process(command):
return subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
def git_config_for(parameter):
config = piped_process(["git", "config", "--get", "--bool", parameter])
(stdout, _) = config.communicate()
return to_text(stdout).strip()
def config_for(parameter):
environment_variable = "flake8_{0}".format(parameter).upper()
git_variable = "flake8.{0}".format(parameter)
value = os.environ.get(environment_variable, git_config_for(git_variable))
return value.lower() in defaults.TRUTHY_VALUES
def update_excludes(exclude_list, temporary_directory_path):
return [
(temporary_directory_path + pattern)
if os.path.isabs(pattern)
else pattern
for pattern in exclude_list
]
def update_paths(checker_manager, temp_prefix):
temp_prefix_length = len(temp_prefix)
for checker in checker_manager.checkers:
filename = checker.display_name
if filename.startswith(temp_prefix):
checker.display_name = os.path.relpath(
filename[temp_prefix_length:]
)
_HOOK_TEMPLATE = """#!{executable}
import sys
from flake8.main import git
if __name__ == '__main__':
sys.exit(
git.hook(
strict=git.config_for('strict'),
lazy=git.config_for('lazy'),
)
)
"""
|