blob: b69d54029ac988df99b88c8a8baeb0ab5d6d7706 [file] [log] [blame]
Éric Araujo0fb681e2011-07-29 12:06:13 +02001#!/usr/bin/env python
Georg Brandlef212e02010-11-26 08:04:57 +00002import re
3import sys
4import shutil
Brett Cannona8b09fd2008-03-18 17:25:13 +00005import os.path
6import subprocess
Éric Araujo35a7f552011-07-30 21:34:04 +02007import sysconfig
Brett Cannona8b09fd2008-03-18 17:25:13 +00008
9import reindent
Georg Brandlef212e02010-11-26 08:04:57 +000010import untabify
11
12
Victor Stinnerd45cb042017-08-17 17:13:01 +020013# Excluded directories which are copies of external libraries:
14# don't check their coding style
15EXCLUDE_DIRS = [os.path.join('Modules', '_ctypes', 'libffi'),
16 os.path.join('Modules', '_ctypes', 'libffi_osx'),
17 os.path.join('Modules', '_ctypes', 'libffi_msvc'),
18 os.path.join('Modules', 'expat'),
19 os.path.join('Modules', 'zlib')]
Éric Araujo35a7f552011-07-30 21:34:04 +020020SRCDIR = sysconfig.get_config_var('srcdir')
21
Victor Stinnerd45cb042017-08-17 17:13:01 +020022
Georg Brandlef212e02010-11-26 08:04:57 +000023def n_files_str(count):
24 """Return 'N file(s)' with the proper plurality on 'file'."""
25 return "{} file{}".format(count, "s" if count != 1 else "")
Brett Cannona8b09fd2008-03-18 17:25:13 +000026
27
28def status(message, modal=False, info=None):
29 """Decorator to output status info to stdout."""
30 def decorated_fxn(fxn):
31 def call_fxn(*args, **kwargs):
32 sys.stdout.write(message + ' ... ')
33 sys.stdout.flush()
34 result = fxn(*args, **kwargs)
35 if not modal and not info:
36 print "done"
37 elif info:
38 print info(result)
39 else:
Georg Brandlef212e02010-11-26 08:04:57 +000040 print "yes" if result else "NO"
Brett Cannona8b09fd2008-03-18 17:25:13 +000041 return result
42 return call_fxn
43 return decorated_fxn
44
Brett Cannona8b09fd2008-03-18 17:25:13 +000045
Nadeem Vawdaf00011a2012-02-22 11:40:09 +020046def mq_patches_applied():
47 """Check if there are any applied MQ patches."""
48 cmd = 'hg qapplied'
49 st = subprocess.Popen(cmd.split(),
50 stdout=subprocess.PIPE,
51 stderr=subprocess.PIPE)
52 try:
53 bstdout, _ = st.communicate()
54 return st.returncode == 0 and bstdout
55 finally:
56 st.stdout.close()
57 st.stderr.close()
58
59
Nick Coghlanc8869af2017-03-12 19:34:16 +100060def get_git_branch():
61 """Get the symbolic name for the current git branch"""
62 cmd = "git rev-parse --abbrev-ref HEAD".split()
63 try:
64 return subprocess.check_output(cmd, stderr=subprocess.PIPE)
65 except subprocess.CalledProcessError:
66 return None
67
68
69def get_git_upstream_remote():
70 """Get the remote name to use for upstream branches
71
72 Uses "upstream" if it exists, "origin" otherwise
73 """
74 cmd = "git remote get-url upstream".split()
75 try:
76 subprocess.check_output(cmd, stderr=subprocess.PIPE)
77 except subprocess.CalledProcessError:
78 return "origin"
79 return "upstream"
80
81
82@status("Getting base branch for PR",
83 info=lambda x: x if x is not None else "not a PR branch")
84def get_base_branch():
Nick Coghland6d943a2017-04-09 18:32:48 +100085 if not os.path.exists(os.path.join(SRCDIR, '.git')):
Nick Coghlanc8869af2017-03-12 19:34:16 +100086 # Not a git checkout, so there's no base branch
87 return None
88 version = sys.version_info
89 if version.releaselevel == 'alpha':
90 base_branch = "master"
91 else:
92 base_branch = "{0.major}.{0.minor}".format(version)
93 this_branch = get_git_branch()
94 if this_branch is None or this_branch == base_branch:
95 # Not on a git PR branch, so there's no base branch
96 return None
97 upstream_remote = get_git_upstream_remote()
98 return upstream_remote + "/" + base_branch
99
100
Georg Brandlef212e02010-11-26 08:04:57 +0000101@status("Getting the list of files that have been added/changed",
102 info=lambda x: n_files_str(len(x)))
Nick Coghlanc8869af2017-03-12 19:34:16 +1000103def changed_files(base_branch=None):
104 """Get the list of changed or added files from Mercurial or git."""
Éric Araujo35a7f552011-07-30 21:34:04 +0200105 if os.path.isdir(os.path.join(SRCDIR, '.hg')):
Nick Coghlanc8869af2017-03-12 19:34:16 +1000106 if base_branch is not None:
107 sys.exit('need a git checkout to check PR status')
Georg Brandlef212e02010-11-26 08:04:57 +0000108 cmd = 'hg status --added --modified --no-status'
Nadeem Vawdaf00011a2012-02-22 11:40:09 +0200109 if mq_patches_applied():
110 cmd += ' --rev qparent'
Nick Coghlanc8869af2017-03-12 19:34:16 +1000111 st = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
112 try:
Victor Stinnerd45cb042017-08-17 17:13:01 +0200113 filenames = [x.decode().rstrip() for x in st.stdout]
Nick Coghlanc8869af2017-03-12 19:34:16 +1000114 finally:
115 st.stdout.close()
Nick Coghlanee10fb92017-03-12 20:03:45 +1000116 elif os.path.exists(os.path.join(SRCDIR, '.git')):
117 # We just use an existence check here as:
118 # directory = normal git checkout/clone
119 # file = git worktree directory
Nick Coghlanc8869af2017-03-12 19:34:16 +1000120 if base_branch:
121 cmd = 'git diff --name-status ' + base_branch
122 else:
123 cmd = 'git status --porcelain'
124 filenames = []
125 st = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
126 try:
127 for line in st.stdout:
128 line = line.decode().rstrip()
129 status_text, filename = line.split(None, 1)
130 status = set(status_text)
131 # modified, added or unmerged files
132 if not status.intersection('MAU'):
133 continue
134 if ' -> ' in filename:
135 # file is renamed
136 filename = filename.split(' -> ', 2)[1].strip()
137 filenames.append(filename)
138 finally:
139 st.stdout.close()
Georg Brandlef212e02010-11-26 08:04:57 +0000140 else:
Victor Stinnerd45cb042017-08-17 17:13:01 +0200141 sys.exit('need a Mercurial or git checkout to get modified files')
142
143 filenames2 = []
144 for filename in filenames:
145 # Normalize the path to be able to match using .startswith()
146 filename = os.path.normpath(filename)
147 if any(filename.startswith(path) for path in EXCLUDE_DIRS):
148 # Exclude the file
149 continue
150 filenames2.append(filename)
151
152 return filenames2
Georg Brandlef212e02010-11-26 08:04:57 +0000153
Georg Brandlef212e02010-11-26 08:04:57 +0000154
155def report_modified_files(file_paths):
156 count = len(file_paths)
157 if count == 0:
158 return n_files_str(count)
159 else:
160 lines = ["{}:".format(n_files_str(count))]
161 for path in file_paths:
162 lines.append(" {}".format(path))
163 return "\n".join(lines)
164
165
166@status("Fixing whitespace", info=report_modified_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000167def normalize_whitespace(file_paths):
168 """Make sure that the whitespace for .py files have been normalized."""
169 reindent.makebackup = False # No need to create backups.
Georg Brandlef212e02010-11-26 08:04:57 +0000170 fixed = []
171 for path in (x for x in file_paths if x.endswith('.py')):
Éric Araujo35a7f552011-07-30 21:34:04 +0200172 if reindent.check(os.path.join(SRCDIR, path)):
Georg Brandlef212e02010-11-26 08:04:57 +0000173 fixed.append(path)
174 return fixed
175
176
177@status("Fixing C file whitespace", info=report_modified_files)
178def normalize_c_whitespace(file_paths):
179 """Report if any C files """
180 fixed = []
181 for path in file_paths:
Éric Araujo35a7f552011-07-30 21:34:04 +0200182 abspath = os.path.join(SRCDIR, path)
183 with open(abspath, 'r') as f:
Georg Brandlef212e02010-11-26 08:04:57 +0000184 if '\t' not in f.read():
185 continue
Éric Araujo35a7f552011-07-30 21:34:04 +0200186 untabify.process(abspath, 8, verbose=False)
Georg Brandlef212e02010-11-26 08:04:57 +0000187 fixed.append(path)
188 return fixed
189
190
191ws_re = re.compile(br'\s+(\r?\n)$')
192
193@status("Fixing docs whitespace", info=report_modified_files)
194def normalize_docs_whitespace(file_paths):
195 fixed = []
196 for path in file_paths:
Éric Araujo35a7f552011-07-30 21:34:04 +0200197 abspath = os.path.join(SRCDIR, path)
Georg Brandlef212e02010-11-26 08:04:57 +0000198 try:
Éric Araujo35a7f552011-07-30 21:34:04 +0200199 with open(abspath, 'rb') as f:
Georg Brandlef212e02010-11-26 08:04:57 +0000200 lines = f.readlines()
201 new_lines = [ws_re.sub(br'\1', line) for line in lines]
202 if new_lines != lines:
Éric Araujo35a7f552011-07-30 21:34:04 +0200203 shutil.copyfile(abspath, abspath + '.bak')
204 with open(abspath, 'wb') as f:
Georg Brandlef212e02010-11-26 08:04:57 +0000205 f.writelines(new_lines)
206 fixed.append(path)
207 except Exception as err:
208 print 'Cannot fix %s: %s' % (path, err)
209 return fixed
210
Brett Cannona8b09fd2008-03-18 17:25:13 +0000211
212@status("Docs modified", modal=True)
213def docs_modified(file_paths):
Georg Brandlef212e02010-11-26 08:04:57 +0000214 """Report if any file in the Doc directory has been changed."""
215 return bool(file_paths)
216
Brett Cannona8b09fd2008-03-18 17:25:13 +0000217
218@status("Misc/ACKS updated", modal=True)
219def credit_given(file_paths):
220 """Check if Misc/ACKS has been changed."""
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400221 return os.path.join('Misc', 'ACKS') in file_paths
Brett Cannona8b09fd2008-03-18 17:25:13 +0000222
Georg Brandlef212e02010-11-26 08:04:57 +0000223
Brett Cannona8b09fd2008-03-18 17:25:13 +0000224@status("Misc/NEWS updated", modal=True)
225def reported_news(file_paths):
226 """Check if Misc/NEWS has been changed."""
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400227 return os.path.join('Misc', 'NEWS') in file_paths
Brett Cannona8b09fd2008-03-18 17:25:13 +0000228
229
230def main():
Nick Coghlanc8869af2017-03-12 19:34:16 +1000231 base_branch = get_base_branch()
232 file_paths = changed_files(base_branch)
Georg Brandlef212e02010-11-26 08:04:57 +0000233 python_files = [fn for fn in file_paths if fn.endswith('.py')]
234 c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))]
Georg Brandl6a1184c2014-10-19 11:54:08 +0200235 doc_files = [fn for fn in file_paths if fn.startswith('Doc') and
236 fn.endswith(('.rst', '.inc'))]
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400237 misc_files = {os.path.join('Misc', 'ACKS'), os.path.join('Misc', 'NEWS')}\
238 & set(file_paths)
Georg Brandlef212e02010-11-26 08:04:57 +0000239 # PEP 8 whitespace rules enforcement.
240 normalize_whitespace(python_files)
241 # C rules enforcement.
242 normalize_c_whitespace(c_files)
243 # Doc whitespace enforcement.
244 normalize_docs_whitespace(doc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000245 # Docs updated.
Georg Brandlef212e02010-11-26 08:04:57 +0000246 docs_modified(doc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000247 # Misc/ACKS changed.
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400248 credit_given(misc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000249 # Misc/NEWS changed.
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400250 reported_news(misc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000251
252 # Test suite run and passed.
Éric Araujoa5afa492011-08-19 08:41:00 +0200253 if python_files or c_files:
Ezio Melotti9e9cb282013-01-11 14:07:47 +0200254 end = " and check for refleaks?" if c_files else "?"
Éric Araujoa5afa492011-08-19 08:41:00 +0200255 print
Ezio Melotti9e9cb282013-01-11 14:07:47 +0200256 print "Did you run the test suite" + end
Brett Cannona8b09fd2008-03-18 17:25:13 +0000257
258
259if __name__ == '__main__':
260 main()