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