1 #!/usr/bin/env python3 2 import re 3 import sys 4 import shutil 5 import os.path 6 import subprocess 7 import sysconfig 8 9 import reindent 10 import untabify 11 12 13 SRCDIR = sysconfig.get_config_var('srcdir') 14 15 16 def 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 21 def 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 39 def mq_patches_applied(): 40 """Check if there are any applied MQ patches.""" 41 cmd = 'hg qapplied' 42 with subprocess.Popen(cmd.split(), 43 stdout=subprocess.PIPE, 44 stderr=subprocess.PIPE) as st: 45 bstdout, _ = st.communicate() 46 return st.returncode == 0 and bstdout 47 48 49 @status("Getting the list of files that have been added/changed", 50 info=lambda x: n_files_str(len(x))) 51 def changed_files(): 52 """Get the list of changed or added files from Mercurial or git.""" 53 if os.path.isdir(os.path.join(SRCDIR, '.hg')): 54 cmd = 'hg status --added --modified --no-status' 55 if mq_patches_applied(): 56 cmd += ' --rev qparent' 57 with subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) as st: 58 return [x.decode().rstrip() for x in st.stdout] 59 elif os.path.isdir(os.path.join(SRCDIR, '.git')): 60 cmd = 'git status --porcelain' 61 filenames = [] 62 with subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) as st: 63 for line in st.stdout: 64 line = line.decode().rstrip() 65 status = set(line[:2]) 66 # modified, added or unmerged files 67 if not status.intersection('MAU'): 68 continue 69 filename = line[3:] 70 if ' -> ' in filename: 71 # file is renamed 72 filename = filename.split(' -> ', 2)[1].strip() 73 filenames.append(filename) 74 return filenames 75 else: 76 sys.exit('need a Mercurial or git checkout to get modified files') 77 78 79 def report_modified_files(file_paths): 80 count = len(file_paths) 81 if count == 0: 82 return n_files_str(count) 83 else: 84 lines = ["{}:".format(n_files_str(count))] 85 for path in file_paths: 86 lines.append(" {}".format(path)) 87 return "\n".join(lines) 88 89 90 @status("Fixing whitespace", info=report_modified_files) 91 def normalize_whitespace(file_paths): 92 """Make sure that the whitespace for .py files have been normalized.""" 93 reindent.makebackup = False # No need to create backups. 94 fixed = [path for path in file_paths if path.endswith('.py') and 95 reindent.check(os.path.join(SRCDIR, path))] 96 return fixed 97 98 99 @status("Fixing C file whitespace", info=report_modified_files) 100 def normalize_c_whitespace(file_paths): 101 """Report if any C files """ 102 fixed = [] 103 for path in file_paths: 104 abspath = os.path.join(SRCDIR, path) 105 with open(abspath, 'r') as f: 106 if '\t' not in f.read(): 107 continue 108 untabify.process(abspath, 8, verbose=False) 109 fixed.append(path) 110 return fixed 111 112 113 ws_re = re.compile(br'\s+(\r?\n)$') 114 115 @status("Fixing docs whitespace", info=report_modified_files) 116 def normalize_docs_whitespace(file_paths): 117 fixed = [] 118 for path in file_paths: 119 abspath = os.path.join(SRCDIR, path) 120 try: 121 with open(abspath, 'rb') as f: 122 lines = f.readlines() 123 new_lines = [ws_re.sub(br'\1', line) for line in lines] 124 if new_lines != lines: 125 shutil.copyfile(abspath, abspath + '.bak') 126 with open(abspath, 'wb') as f: 127 f.writelines(new_lines) 128 fixed.append(path) 129 except Exception as err: 130 print('Cannot fix %s: %s' % (path, err)) 131 return fixed 132 133 134 @status("Docs modified", modal=True) 135 def docs_modified(file_paths): 136 """Report if any file in the Doc directory has been changed.""" 137 return bool(file_paths) 138 139 140 @status("Misc/ACKS updated", modal=True) 141 def credit_given(file_paths): 142 """Check if Misc/ACKS has been changed.""" 143 return os.path.join('Misc', 'ACKS') in file_paths 144 145 146 @status("Misc/NEWS updated", modal=True) 147 def reported_news(file_paths): 148 """Check if Misc/NEWS has been changed.""" 149 return os.path.join('Misc', 'NEWS') in file_paths 150 151 @status("configure regenerated", modal=True, info=str) 152 def regenerated_configure(file_paths): 153 """Check if configure has been regenerated.""" 154 if 'configure.ac' in file_paths: 155 return "yes" if 'configure' in file_paths else "no" 156 else: 157 return "not needed" 158 159 @status("pyconfig.h.in regenerated", modal=True, info=str) 160 def regenerated_pyconfig_h_in(file_paths): 161 """Check if pyconfig.h.in has been regenerated.""" 162 if 'configure.ac' in file_paths: 163 return "yes" if 'pyconfig.h.in' in file_paths else "no" 164 else: 165 return "not needed" 166 167 def main(): 168 file_paths = changed_files() 169 python_files = [fn for fn in file_paths if fn.endswith('.py')] 170 c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))] 171 doc_files = [fn for fn in file_paths if fn.startswith('Doc') and 172 fn.endswith(('.rst', '.inc'))] 173 misc_files = {os.path.join('Misc', 'ACKS'), os.path.join('Misc', 'NEWS')}\ 174 & set(file_paths) 175 # PEP 8 whitespace rules enforcement. 176 normalize_whitespace(python_files) 177 # C rules enforcement. 178 normalize_c_whitespace(c_files) 179 # Doc whitespace enforcement. 180 normalize_docs_whitespace(doc_files) 181 # Docs updated. 182 docs_modified(doc_files) 183 # Misc/ACKS changed. 184 credit_given(misc_files) 185 # Misc/NEWS changed. 186 reported_news(misc_files) 187 # Regenerated configure, if necessary. 188 regenerated_configure(file_paths) 189 # Regenerated pyconfig.h.in, if necessary. 190 regenerated_pyconfig_h_in(file_paths) 191 192 # Test suite run and passed. 193 if python_files or c_files: 194 end = " and check for refleaks?" if c_files else "?" 195 print() 196 print("Did you run the test suite" + end) 197 198 199 if __name__ == '__main__': 200 main() 201