-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcheckpatch.py
executable file
·187 lines (154 loc) · 5.88 KB
/
checkpatch.py
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
#!/usr/bin/env python3
import re
import os
import subprocess
import sys
import shutil
import ast
from argparse import ArgumentParser
trailing_w_re = re.compile(r'\s+\n$')
only_w_re = re.compile(r'^\s+\n$')
def check_whitespace(name, correct=False):
found_problems = False
if correct:
dst = open(name + '.checkpatch.tmp', 'w+')
with open(name, 'r+') as fh:
for (lineno, line) in enumerate(fh):
if only_w_re.search(line):
print('%s:%d: line consists only of whitespace' % (name, lineno+1))
found_problems = True
elif trailing_w_re.search(line):
print('%s:%d: trailing whitespace' % (name, lineno+1))
found_problems = True
if correct:
dst.write(line.rstrip() + '\n')
if correct:
fh.seek(0)
dst.seek(0)
shutil.copyfileobj(dst, fh)
fh.truncate()
os.unlink(dst.name)
found_problems = False
return found_problems
def get_definitions(path):
'''Yield all objects defined directly in *path*
This does not include imported objects, or objects defined
dynamically (e.g. by using eval, or modifying globals()).
'''
names = set()
for node in ast.parse(open(path, 'rb').read()).body:
for name in _iter_definitions(node):
names.add(name)
return names
def _iter_definitions(node):
if isinstance(node, ast.Assign):
for node in node.targets:
while isinstance(node, ast.Attribute):
node = node.value
assert isinstance(node, ast.Name)
yield node.id
elif isinstance(node, (ast.FunctionDef, ast.ClassDef)):
yield node.name
elif isinstance(node, ast.If):
for snode in node.body:
yield from _iter_definitions(snode)
for snode in node.orelse:
yield from _iter_definitions(snode)
elif isinstance(node, ast.Try):
for snode in (node.body, node.finalbody, node.orelse):
for ssnode in snode:
yield from _iter_definitions(ssnode)
for snode in node.handlers:
assert isinstance(snode, ast.ExceptHandler)
for ssnode in snode.body:
yield from _iter_definitions(ssnode)
def iter_imports(path):
'''Yield imports in *path*'''
for node in ast.parse(open(path, 'rb').read()).body:
if isinstance(node, ast.ImportFrom):
if node.module is None:
prefix = ()
else:
prefix = tuple(node.module.split('.'))
for snode in node.names:
yield (node.level, prefix + (snode.name,))
elif isinstance(node, ast.Import):
for node in node.names:
yield (0, tuple(node.name.split('.')))
def yield_modules(path):
'''Yield all Python modules underneath *path*'''
for (dpath, dnames, fnames) in os.walk(path):
module = tuple(dpath.split('/')[1:])
for fname in fnames:
if not fname.endswith('.py'):
continue
fpath = os.path.join(dpath, fname)
if fname == '__init__.py':
yield (fpath, module)
else:
yield (fpath, module + (fname[:-3],))
dnames[:] = [ x for x in dnames
if os.path.exists(os.path.join(dpath, x, '__init__.py')) ]
def check_imports():
'''Check if all imports are direct'''
# Memorize where objects are defined
definitions = dict()
for (fpath, modname) in yield_modules('src'):
definitions[modname] = get_definitions(fpath)
# Special case, we always want to import these indirectly
definitions['s3ql', 'logging'].add('logging')
definitions['s3ql',].add('ROOT_INODE')
# False positives
definitions['s3ql',].add('deltadump')
# Check if imports are direct
found_problems = False
for path in ('src', 'util', 'contrib', 'tests'):
for (fpath, modname) in yield_modules(path):
for (lvl, name) in iter_imports(fpath):
if lvl:
if lvl and fpath.endswith('/__init__.py'):
lvl += 1
name = modname[:len(modname)-lvl] + name
if name in definitions:
# Import of entire module
continue
mod_idx = len(name)-1
while mod_idx > 0:
if name[:mod_idx] in definitions:
break
mod_idx -= 1
else:
# No definitions on record
continue
if name[mod_idx] not in definitions[name[:mod_idx]]:
print('%s imports %s from %s, but is defined elsewhere'
% (fpath, name[mod_idx], '.'.join(name[:mod_idx])))
found_problems = True
return found_problems
def check_pyflakes(name):
return subprocess.call(['pyflakes3', name]) == 1
def parse_args():
parser = ArgumentParser(
description="Check if tracked files are ready for commit")
parser.add_argument("--fix-whitespace", action="store_true", default=False,
help="Automatically correct whitespace problems")
return parser.parse_args()
options = parse_args()
os.chdir(os.path.dirname(__file__))
found_problems = False
if not check_imports():
found_problems = True
hg_out = subprocess.check_output(['hg', 'status', '--modified', '--added',
'--no-status', '--print0'])
for b_name in hg_out.split(b'\0'):
if not b_name:
continue
name = b_name.decode('utf-8', errors='surrogatescape')
if check_whitespace(name, correct=options.fix_whitespace):
found_problems = True
if name.endswith('.py') and check_pyflakes(name):
found_problems = True
if found_problems:
sys.exit(1)
else:
sys.exit(0)