blob: 58aa168d6d06131fa8df8fa6632b5f9f95f55935 [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
Nick Coghlanc8869af2017-03-12 19:34:16 +100046def get_git_branch():
47 """Get the symbolic name for the current git branch"""
48 cmd = "git rev-parse --abbrev-ref HEAD".split()
49 try:
50 return subprocess.check_output(cmd, stderr=subprocess.PIPE)
51 except subprocess.CalledProcessError:
52 return None
53
54
55def get_git_upstream_remote():
56 """Get the remote name to use for upstream branches
57
58 Uses "upstream" if it exists, "origin" otherwise
59 """
60 cmd = "git remote get-url upstream".split()
61 try:
62 subprocess.check_output(cmd, stderr=subprocess.PIPE)
63 except subprocess.CalledProcessError:
64 return "origin"
65 return "upstream"
66
67
68@status("Getting base branch for PR",
69 info=lambda x: x if x is not None else "not a PR branch")
70def get_base_branch():
Nick Coghland6d943a2017-04-09 18:32:48 +100071 if not os.path.exists(os.path.join(SRCDIR, '.git')):
Nick Coghlanc8869af2017-03-12 19:34:16 +100072 # Not a git checkout, so there's no base branch
73 return None
74 version = sys.version_info
75 if version.releaselevel == 'alpha':
76 base_branch = "master"
77 else:
78 base_branch = "{0.major}.{0.minor}".format(version)
79 this_branch = get_git_branch()
80 if this_branch is None or this_branch == base_branch:
81 # Not on a git PR branch, so there's no base branch
82 return None
83 upstream_remote = get_git_upstream_remote()
84 return upstream_remote + "/" + base_branch
85
86
Georg Brandlef212e02010-11-26 08:04:57 +000087@status("Getting the list of files that have been added/changed",
88 info=lambda x: n_files_str(len(x)))
Nick Coghlanc8869af2017-03-12 19:34:16 +100089def changed_files(base_branch=None):
Benjamin Peterson9bbb8e22018-06-05 22:55:10 -070090 """Get the list of changed or added files from git."""
91 if os.path.exists(os.path.join(SRCDIR, '.git')):
Nick Coghlanee10fb92017-03-12 20:03:45 +100092 # We just use an existence check here as:
93 # directory = normal git checkout/clone
94 # file = git worktree directory
Nick Coghlanc8869af2017-03-12 19:34:16 +100095 if base_branch:
96 cmd = 'git diff --name-status ' + base_branch
97 else:
98 cmd = 'git status --porcelain'
99 filenames = []
100 st = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
101 try:
102 for line in st.stdout:
103 line = line.decode().rstrip()
104 status_text, filename = line.split(None, 1)
105 status = set(status_text)
106 # modified, added or unmerged files
107 if not status.intersection('MAU'):
108 continue
109 if ' -> ' in filename:
110 # file is renamed
111 filename = filename.split(' -> ', 2)[1].strip()
112 filenames.append(filename)
113 finally:
114 st.stdout.close()
Georg Brandlef212e02010-11-26 08:04:57 +0000115 else:
Benjamin Peterson9bbb8e22018-06-05 22:55:10 -0700116 sys.exit('need a git checkout to get modified files')
Victor Stinnerd45cb042017-08-17 17:13:01 +0200117
118 filenames2 = []
119 for filename in filenames:
120 # Normalize the path to be able to match using .startswith()
121 filename = os.path.normpath(filename)
122 if any(filename.startswith(path) for path in EXCLUDE_DIRS):
123 # Exclude the file
124 continue
125 filenames2.append(filename)
126
127 return filenames2
Georg Brandlef212e02010-11-26 08:04:57 +0000128
Georg Brandlef212e02010-11-26 08:04:57 +0000129
130def report_modified_files(file_paths):
131 count = len(file_paths)
132 if count == 0:
133 return n_files_str(count)
134 else:
135 lines = ["{}:".format(n_files_str(count))]
136 for path in file_paths:
137 lines.append(" {}".format(path))
138 return "\n".join(lines)
139
140
141@status("Fixing whitespace", info=report_modified_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000142def normalize_whitespace(file_paths):
143 """Make sure that the whitespace for .py files have been normalized."""
144 reindent.makebackup = False # No need to create backups.
Georg Brandlef212e02010-11-26 08:04:57 +0000145 fixed = []
146 for path in (x for x in file_paths if x.endswith('.py')):
Éric Araujo35a7f552011-07-30 21:34:04 +0200147 if reindent.check(os.path.join(SRCDIR, path)):
Georg Brandlef212e02010-11-26 08:04:57 +0000148 fixed.append(path)
149 return fixed
150
151
152@status("Fixing C file whitespace", info=report_modified_files)
153def normalize_c_whitespace(file_paths):
154 """Report if any C files """
155 fixed = []
156 for path in file_paths:
Éric Araujo35a7f552011-07-30 21:34:04 +0200157 abspath = os.path.join(SRCDIR, path)
158 with open(abspath, 'r') as f:
Georg Brandlef212e02010-11-26 08:04:57 +0000159 if '\t' not in f.read():
160 continue
Éric Araujo35a7f552011-07-30 21:34:04 +0200161 untabify.process(abspath, 8, verbose=False)
Georg Brandlef212e02010-11-26 08:04:57 +0000162 fixed.append(path)
163 return fixed
164
165
166ws_re = re.compile(br'\s+(\r?\n)$')
167
168@status("Fixing docs whitespace", info=report_modified_files)
169def normalize_docs_whitespace(file_paths):
170 fixed = []
171 for path in file_paths:
Éric Araujo35a7f552011-07-30 21:34:04 +0200172 abspath = os.path.join(SRCDIR, path)
Georg Brandlef212e02010-11-26 08:04:57 +0000173 try:
Éric Araujo35a7f552011-07-30 21:34:04 +0200174 with open(abspath, 'rb') as f:
Georg Brandlef212e02010-11-26 08:04:57 +0000175 lines = f.readlines()
176 new_lines = [ws_re.sub(br'\1', line) for line in lines]
177 if new_lines != lines:
Éric Araujo35a7f552011-07-30 21:34:04 +0200178 shutil.copyfile(abspath, abspath + '.bak')
179 with open(abspath, 'wb') as f:
Georg Brandlef212e02010-11-26 08:04:57 +0000180 f.writelines(new_lines)
181 fixed.append(path)
182 except Exception as err:
183 print 'Cannot fix %s: %s' % (path, err)
184 return fixed
185
Brett Cannona8b09fd2008-03-18 17:25:13 +0000186
187@status("Docs modified", modal=True)
188def docs_modified(file_paths):
Georg Brandlef212e02010-11-26 08:04:57 +0000189 """Report if any file in the Doc directory has been changed."""
190 return bool(file_paths)
191
Brett Cannona8b09fd2008-03-18 17:25:13 +0000192
193@status("Misc/ACKS updated", modal=True)
194def credit_given(file_paths):
195 """Check if Misc/ACKS has been changed."""
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400196 return os.path.join('Misc', 'ACKS') in file_paths
Brett Cannona8b09fd2008-03-18 17:25:13 +0000197
Georg Brandlef212e02010-11-26 08:04:57 +0000198
Antoine Pitrou36af1182018-04-23 14:22:15 +0200199@status("Misc/NEWS.d updated with `blurb`", modal=True)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000200def reported_news(file_paths):
Antoine Pitrou36af1182018-04-23 14:22:15 +0200201 """Check if Misc/NEWS.d has been changed."""
202 return any(p.startswith(os.path.join('Misc', 'NEWS.d', 'next'))
203 for p in file_paths)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000204
205
206def main():
Nick Coghlanc8869af2017-03-12 19:34:16 +1000207 base_branch = get_base_branch()
208 file_paths = changed_files(base_branch)
Georg Brandlef212e02010-11-26 08:04:57 +0000209 python_files = [fn for fn in file_paths if fn.endswith('.py')]
210 c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))]
Georg Brandl6a1184c2014-10-19 11:54:08 +0200211 doc_files = [fn for fn in file_paths if fn.startswith('Doc') and
212 fn.endswith(('.rst', '.inc'))]
Antoine Pitrou36af1182018-04-23 14:22:15 +0200213 misc_files = {p for p in file_paths if p.startswith('Misc')}
Georg Brandlef212e02010-11-26 08:04:57 +0000214 # PEP 8 whitespace rules enforcement.
215 normalize_whitespace(python_files)
216 # C rules enforcement.
217 normalize_c_whitespace(c_files)
218 # Doc whitespace enforcement.
219 normalize_docs_whitespace(doc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000220 # Docs updated.
Georg Brandlef212e02010-11-26 08:04:57 +0000221 docs_modified(doc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000222 # Misc/ACKS changed.
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400223 credit_given(misc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000224 # Misc/NEWS changed.
Terry Jan Reedy68ad1d12013-07-21 20:57:44 -0400225 reported_news(misc_files)
Brett Cannona8b09fd2008-03-18 17:25:13 +0000226
227 # Test suite run and passed.
Éric Araujoa5afa492011-08-19 08:41:00 +0200228 if python_files or c_files:
Ezio Melotti9e9cb282013-01-11 14:07:47 +0200229 end = " and check for refleaks?" if c_files else "?"
Éric Araujoa5afa492011-08-19 08:41:00 +0200230 print
Ezio Melotti9e9cb282013-01-11 14:07:47 +0200231 print "Did you run the test suite" + end
Brett Cannona8b09fd2008-03-18 17:25:13 +0000232
233
234if __name__ == '__main__':
235 main()