1#!/usr/bin/env python 2import re 3import sys 4import shutil 5import os.path 6import subprocess 7import sysconfig 8 9import reindent 10import untabify 11 12 13SRCDIR = sysconfig.get_config_var('srcdir') 14 15 16def n_files_str(count): 17 """Return 'N file(s)' with the proper plurality on 'file'.""" 18 return "{} file{}".format(count, "s" if count != 1 else "") 19 20 21def status(message, modal=False, info=None): 22 """Decorator to output status info to stdout.""" 23 def decorated_fxn(fxn): 24 def call_fxn(*args, **kwargs): 25 sys.stdout.write(message + ' ... ') 26 sys.stdout.flush() 27 result = fxn(*args, **kwargs) 28 if not modal and not info: 29 print "done" 30 elif info: 31 print info(result) 32 else: 33 print "yes" if result else "NO" 34 return result 35 return call_fxn 36 return decorated_fxn 37 38 39def mq_patches_applied(): 40 """Check if there are any applied MQ patches.""" 41 cmd = 'hg qapplied' 42 st = subprocess.Popen(cmd.split(), 43 stdout=subprocess.PIPE, 44 stderr=subprocess.PIPE) 45 try: 46 bstdout, _ = st.communicate() 47 return st.returncode == 0 and bstdout 48 finally: 49 st.stdout.close() 50 st.stderr.close() 51 52 53@status("Getting the list of files that have been added/changed", 54 info=lambda x: n_files_str(len(x))) 55def changed_files(): 56 """Get the list of changed or added files from the VCS.""" 57 if os.path.isdir(os.path.join(SRCDIR, '.hg')): 58 vcs = 'hg' 59 cmd = 'hg status --added --modified --no-status' 60 if mq_patches_applied(): 61 cmd += ' --rev qparent' 62 elif os.path.isdir('.svn'): 63 vcs = 'svn' 64 cmd = 'svn status --quiet --non-interactive --ignore-externals' 65 else: 66 sys.exit('need a checkout to get modified files') 67 68 st = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) 69 try: 70 st.wait() 71 if vcs == 'hg': 72 return [x.decode().rstrip() for x in st.stdout] 73 else: 74 output = (x.decode().rstrip().rsplit(None, 1)[-1] 75 for x in st.stdout if x[0] in 'AM') 76 return set(path for path in output if os.path.isfile(path)) 77 finally: 78 st.stdout.close() 79 80 81def report_modified_files(file_paths): 82 count = len(file_paths) 83 if count == 0: 84 return n_files_str(count) 85 else: 86 lines = ["{}:".format(n_files_str(count))] 87 for path in file_paths: 88 lines.append(" {}".format(path)) 89 return "\n".join(lines) 90 91 92@status("Fixing whitespace", info=report_modified_files) 93def normalize_whitespace(file_paths): 94 """Make sure that the whitespace for .py files have been normalized.""" 95 reindent.makebackup = False # No need to create backups. 96 fixed = [] 97 for path in (x for x in file_paths if x.endswith('.py')): 98 if reindent.check(os.path.join(SRCDIR, path)): 99 fixed.append(path) 100 return fixed 101 102 103@status("Fixing C file whitespace", info=report_modified_files) 104def normalize_c_whitespace(file_paths): 105 """Report if any C files """ 106 fixed = [] 107 for path in file_paths: 108 abspath = os.path.join(SRCDIR, path) 109 with open(abspath, 'r') as f: 110 if '\t' not in f.read(): 111 continue 112 untabify.process(abspath, 8, verbose=False) 113 fixed.append(path) 114 return fixed 115 116 117ws_re = re.compile(br'\s+(\r?\n)$') 118 119@status("Fixing docs whitespace", info=report_modified_files) 120def normalize_docs_whitespace(file_paths): 121 fixed = [] 122 for path in file_paths: 123 abspath = os.path.join(SRCDIR, path) 124 try: 125 with open(abspath, 'rb') as f: 126 lines = f.readlines() 127 new_lines = [ws_re.sub(br'\1', line) for line in lines] 128 if new_lines != lines: 129 shutil.copyfile(abspath, abspath + '.bak') 130 with open(abspath, 'wb') as f: 131 f.writelines(new_lines) 132 fixed.append(path) 133 except Exception as err: 134 print 'Cannot fix %s: %s' % (path, err) 135 return fixed 136 137 138@status("Docs modified", modal=True) 139def docs_modified(file_paths): 140 """Report if any file in the Doc directory has been changed.""" 141 return bool(file_paths) 142 143 144@status("Misc/ACKS updated", modal=True) 145def credit_given(file_paths): 146 """Check if Misc/ACKS has been changed.""" 147 return os.path.join('Misc', 'ACKS') in file_paths 148 149 150@status("Misc/NEWS updated", modal=True) 151def reported_news(file_paths): 152 """Check if Misc/NEWS has been changed.""" 153 return os.path.join('Misc', 'NEWS') in file_paths 154 155 156def main(): 157 file_paths = changed_files() 158 python_files = [fn for fn in file_paths if fn.endswith('.py')] 159 c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))] 160 doc_files = [fn for fn in file_paths if fn.startswith('Doc') and 161 fn.endswith(('.rst', '.inc'))] 162 misc_files = {os.path.join('Misc', 'ACKS'), os.path.join('Misc', 'NEWS')}\ 163 & set(file_paths) 164 # PEP 8 whitespace rules enforcement. 165 normalize_whitespace(python_files) 166 # C rules enforcement. 167 normalize_c_whitespace(c_files) 168 # Doc whitespace enforcement. 169 normalize_docs_whitespace(doc_files) 170 # Docs updated. 171 docs_modified(doc_files) 172 # Misc/ACKS changed. 173 credit_given(misc_files) 174 # Misc/NEWS changed. 175 reported_news(misc_files) 176 177 # Test suite run and passed. 178 if python_files or c_files: 179 end = " and check for refleaks?" if c_files else "?" 180 print 181 print "Did you run the test suite" + end 182 183 184if __name__ == '__main__': 185 main() 186