17db96d56Sopenharmony_ci#!/usr/bin/env python3 27db96d56Sopenharmony_ci"""Check proposed changes for common issues.""" 37db96d56Sopenharmony_ciimport re 47db96d56Sopenharmony_ciimport sys 57db96d56Sopenharmony_ciimport shutil 67db96d56Sopenharmony_ciimport os.path 77db96d56Sopenharmony_ciimport subprocess 87db96d56Sopenharmony_ciimport sysconfig 97db96d56Sopenharmony_ci 107db96d56Sopenharmony_ciimport reindent 117db96d56Sopenharmony_ciimport untabify 127db96d56Sopenharmony_ci 137db96d56Sopenharmony_ci 147db96d56Sopenharmony_ci# Excluded directories which are copies of external libraries: 157db96d56Sopenharmony_ci# don't check their coding style 167db96d56Sopenharmony_ciEXCLUDE_DIRS = [os.path.join('Modules', '_ctypes', 'libffi_osx'), 177db96d56Sopenharmony_ci os.path.join('Modules', '_ctypes', 'libffi_msvc'), 187db96d56Sopenharmony_ci os.path.join('Modules', '_decimal', 'libmpdec'), 197db96d56Sopenharmony_ci os.path.join('Modules', 'expat'), 207db96d56Sopenharmony_ci os.path.join('Modules', 'zlib')] 217db96d56Sopenharmony_ciSRCDIR = sysconfig.get_config_var('srcdir') 227db96d56Sopenharmony_ci 237db96d56Sopenharmony_ci 247db96d56Sopenharmony_cidef n_files_str(count): 257db96d56Sopenharmony_ci """Return 'N file(s)' with the proper plurality on 'file'.""" 267db96d56Sopenharmony_ci return "{} file{}".format(count, "s" if count != 1 else "") 277db96d56Sopenharmony_ci 287db96d56Sopenharmony_ci 297db96d56Sopenharmony_cidef status(message, modal=False, info=None): 307db96d56Sopenharmony_ci """Decorator to output status info to stdout.""" 317db96d56Sopenharmony_ci def decorated_fxn(fxn): 327db96d56Sopenharmony_ci def call_fxn(*args, **kwargs): 337db96d56Sopenharmony_ci sys.stdout.write(message + ' ... ') 347db96d56Sopenharmony_ci sys.stdout.flush() 357db96d56Sopenharmony_ci result = fxn(*args, **kwargs) 367db96d56Sopenharmony_ci if not modal and not info: 377db96d56Sopenharmony_ci print("done") 387db96d56Sopenharmony_ci elif info: 397db96d56Sopenharmony_ci print(info(result)) 407db96d56Sopenharmony_ci else: 417db96d56Sopenharmony_ci print("yes" if result else "NO") 427db96d56Sopenharmony_ci return result 437db96d56Sopenharmony_ci return call_fxn 447db96d56Sopenharmony_ci return decorated_fxn 457db96d56Sopenharmony_ci 467db96d56Sopenharmony_ci 477db96d56Sopenharmony_cidef get_git_branch(): 487db96d56Sopenharmony_ci """Get the symbolic name for the current git branch""" 497db96d56Sopenharmony_ci cmd = "git rev-parse --abbrev-ref HEAD".split() 507db96d56Sopenharmony_ci try: 517db96d56Sopenharmony_ci return subprocess.check_output(cmd, 527db96d56Sopenharmony_ci stderr=subprocess.DEVNULL, 537db96d56Sopenharmony_ci cwd=SRCDIR, 547db96d56Sopenharmony_ci encoding='UTF-8') 557db96d56Sopenharmony_ci except subprocess.CalledProcessError: 567db96d56Sopenharmony_ci return None 577db96d56Sopenharmony_ci 587db96d56Sopenharmony_ci 597db96d56Sopenharmony_cidef get_git_upstream_remote(): 607db96d56Sopenharmony_ci """Get the remote name to use for upstream branches 617db96d56Sopenharmony_ci 627db96d56Sopenharmony_ci Uses "upstream" if it exists, "origin" otherwise 637db96d56Sopenharmony_ci """ 647db96d56Sopenharmony_ci cmd = "git remote get-url upstream".split() 657db96d56Sopenharmony_ci try: 667db96d56Sopenharmony_ci subprocess.check_output(cmd, 677db96d56Sopenharmony_ci stderr=subprocess.DEVNULL, 687db96d56Sopenharmony_ci cwd=SRCDIR, 697db96d56Sopenharmony_ci encoding='UTF-8') 707db96d56Sopenharmony_ci except subprocess.CalledProcessError: 717db96d56Sopenharmony_ci return "origin" 727db96d56Sopenharmony_ci return "upstream" 737db96d56Sopenharmony_ci 747db96d56Sopenharmony_ci 757db96d56Sopenharmony_cidef get_git_remote_default_branch(remote_name): 767db96d56Sopenharmony_ci """Get the name of the default branch for the given remote 777db96d56Sopenharmony_ci 787db96d56Sopenharmony_ci It is typically called 'main', but may differ 797db96d56Sopenharmony_ci """ 807db96d56Sopenharmony_ci cmd = "git remote show {}".format(remote_name).split() 817db96d56Sopenharmony_ci env = os.environ.copy() 827db96d56Sopenharmony_ci env['LANG'] = 'C' 837db96d56Sopenharmony_ci try: 847db96d56Sopenharmony_ci remote_info = subprocess.check_output(cmd, 857db96d56Sopenharmony_ci stderr=subprocess.DEVNULL, 867db96d56Sopenharmony_ci cwd=SRCDIR, 877db96d56Sopenharmony_ci encoding='UTF-8', 887db96d56Sopenharmony_ci env=env) 897db96d56Sopenharmony_ci except subprocess.CalledProcessError: 907db96d56Sopenharmony_ci return None 917db96d56Sopenharmony_ci for line in remote_info.splitlines(): 927db96d56Sopenharmony_ci if "HEAD branch:" in line: 937db96d56Sopenharmony_ci base_branch = line.split(":")[1].strip() 947db96d56Sopenharmony_ci return base_branch 957db96d56Sopenharmony_ci return None 967db96d56Sopenharmony_ci 977db96d56Sopenharmony_ci 987db96d56Sopenharmony_ci@status("Getting base branch for PR", 997db96d56Sopenharmony_ci info=lambda x: x if x is not None else "not a PR branch") 1007db96d56Sopenharmony_cidef get_base_branch(): 1017db96d56Sopenharmony_ci if not os.path.exists(os.path.join(SRCDIR, '.git')): 1027db96d56Sopenharmony_ci # Not a git checkout, so there's no base branch 1037db96d56Sopenharmony_ci return None 1047db96d56Sopenharmony_ci upstream_remote = get_git_upstream_remote() 1057db96d56Sopenharmony_ci version = sys.version_info 1067db96d56Sopenharmony_ci if version.releaselevel == 'alpha': 1077db96d56Sopenharmony_ci base_branch = get_git_remote_default_branch(upstream_remote) 1087db96d56Sopenharmony_ci else: 1097db96d56Sopenharmony_ci base_branch = "{0.major}.{0.minor}".format(version) 1107db96d56Sopenharmony_ci this_branch = get_git_branch() 1117db96d56Sopenharmony_ci if this_branch is None or this_branch == base_branch: 1127db96d56Sopenharmony_ci # Not on a git PR branch, so there's no base branch 1137db96d56Sopenharmony_ci return None 1147db96d56Sopenharmony_ci return upstream_remote + "/" + base_branch 1157db96d56Sopenharmony_ci 1167db96d56Sopenharmony_ci 1177db96d56Sopenharmony_ci@status("Getting the list of files that have been added/changed", 1187db96d56Sopenharmony_ci info=lambda x: n_files_str(len(x))) 1197db96d56Sopenharmony_cidef changed_files(base_branch=None): 1207db96d56Sopenharmony_ci """Get the list of changed or added files from git.""" 1217db96d56Sopenharmony_ci if os.path.exists(os.path.join(SRCDIR, '.git')): 1227db96d56Sopenharmony_ci # We just use an existence check here as: 1237db96d56Sopenharmony_ci # directory = normal git checkout/clone 1247db96d56Sopenharmony_ci # file = git worktree directory 1257db96d56Sopenharmony_ci if base_branch: 1267db96d56Sopenharmony_ci cmd = 'git diff --name-status ' + base_branch 1277db96d56Sopenharmony_ci else: 1287db96d56Sopenharmony_ci cmd = 'git status --porcelain' 1297db96d56Sopenharmony_ci filenames = [] 1307db96d56Sopenharmony_ci with subprocess.Popen(cmd.split(), 1317db96d56Sopenharmony_ci stdout=subprocess.PIPE, 1327db96d56Sopenharmony_ci cwd=SRCDIR) as st: 1337db96d56Sopenharmony_ci if st.wait() != 0: 1347db96d56Sopenharmony_ci sys.exit(f'error running {cmd}') 1357db96d56Sopenharmony_ci for line in st.stdout: 1367db96d56Sopenharmony_ci line = line.decode().rstrip() 1377db96d56Sopenharmony_ci status_text, filename = line.split(maxsplit=1) 1387db96d56Sopenharmony_ci status = set(status_text) 1397db96d56Sopenharmony_ci # modified, added or unmerged files 1407db96d56Sopenharmony_ci if not status.intersection('MAU'): 1417db96d56Sopenharmony_ci continue 1427db96d56Sopenharmony_ci if ' -> ' in filename: 1437db96d56Sopenharmony_ci # file is renamed 1447db96d56Sopenharmony_ci filename = filename.split(' -> ', 2)[1].strip() 1457db96d56Sopenharmony_ci filenames.append(filename) 1467db96d56Sopenharmony_ci else: 1477db96d56Sopenharmony_ci sys.exit('need a git checkout to get modified files') 1487db96d56Sopenharmony_ci 1497db96d56Sopenharmony_ci filenames2 = [] 1507db96d56Sopenharmony_ci for filename in filenames: 1517db96d56Sopenharmony_ci # Normalize the path to be able to match using .startswith() 1527db96d56Sopenharmony_ci filename = os.path.normpath(filename) 1537db96d56Sopenharmony_ci if any(filename.startswith(path) for path in EXCLUDE_DIRS): 1547db96d56Sopenharmony_ci # Exclude the file 1557db96d56Sopenharmony_ci continue 1567db96d56Sopenharmony_ci filenames2.append(filename) 1577db96d56Sopenharmony_ci 1587db96d56Sopenharmony_ci return filenames2 1597db96d56Sopenharmony_ci 1607db96d56Sopenharmony_ci 1617db96d56Sopenharmony_cidef report_modified_files(file_paths): 1627db96d56Sopenharmony_ci count = len(file_paths) 1637db96d56Sopenharmony_ci if count == 0: 1647db96d56Sopenharmony_ci return n_files_str(count) 1657db96d56Sopenharmony_ci else: 1667db96d56Sopenharmony_ci lines = ["{}:".format(n_files_str(count))] 1677db96d56Sopenharmony_ci for path in file_paths: 1687db96d56Sopenharmony_ci lines.append(" {}".format(path)) 1697db96d56Sopenharmony_ci return "\n".join(lines) 1707db96d56Sopenharmony_ci 1717db96d56Sopenharmony_ci 1727db96d56Sopenharmony_ci@status("Fixing Python file whitespace", info=report_modified_files) 1737db96d56Sopenharmony_cidef normalize_whitespace(file_paths): 1747db96d56Sopenharmony_ci """Make sure that the whitespace for .py files have been normalized.""" 1757db96d56Sopenharmony_ci reindent.makebackup = False # No need to create backups. 1767db96d56Sopenharmony_ci fixed = [path for path in file_paths if path.endswith('.py') and 1777db96d56Sopenharmony_ci reindent.check(os.path.join(SRCDIR, path))] 1787db96d56Sopenharmony_ci return fixed 1797db96d56Sopenharmony_ci 1807db96d56Sopenharmony_ci 1817db96d56Sopenharmony_ci@status("Fixing C file whitespace", info=report_modified_files) 1827db96d56Sopenharmony_cidef normalize_c_whitespace(file_paths): 1837db96d56Sopenharmony_ci """Report if any C files """ 1847db96d56Sopenharmony_ci fixed = [] 1857db96d56Sopenharmony_ci for path in file_paths: 1867db96d56Sopenharmony_ci abspath = os.path.join(SRCDIR, path) 1877db96d56Sopenharmony_ci with open(abspath, 'r') as f: 1887db96d56Sopenharmony_ci if '\t' not in f.read(): 1897db96d56Sopenharmony_ci continue 1907db96d56Sopenharmony_ci untabify.process(abspath, 8, verbose=False) 1917db96d56Sopenharmony_ci fixed.append(path) 1927db96d56Sopenharmony_ci return fixed 1937db96d56Sopenharmony_ci 1947db96d56Sopenharmony_ci 1957db96d56Sopenharmony_ciws_re = re.compile(br'\s+(\r?\n)$') 1967db96d56Sopenharmony_ci 1977db96d56Sopenharmony_ci@status("Fixing docs whitespace", info=report_modified_files) 1987db96d56Sopenharmony_cidef normalize_docs_whitespace(file_paths): 1997db96d56Sopenharmony_ci fixed = [] 2007db96d56Sopenharmony_ci for path in file_paths: 2017db96d56Sopenharmony_ci abspath = os.path.join(SRCDIR, path) 2027db96d56Sopenharmony_ci try: 2037db96d56Sopenharmony_ci with open(abspath, 'rb') as f: 2047db96d56Sopenharmony_ci lines = f.readlines() 2057db96d56Sopenharmony_ci new_lines = [ws_re.sub(br'\1', line) for line in lines] 2067db96d56Sopenharmony_ci if new_lines != lines: 2077db96d56Sopenharmony_ci shutil.copyfile(abspath, abspath + '.bak') 2087db96d56Sopenharmony_ci with open(abspath, 'wb') as f: 2097db96d56Sopenharmony_ci f.writelines(new_lines) 2107db96d56Sopenharmony_ci fixed.append(path) 2117db96d56Sopenharmony_ci except Exception as err: 2127db96d56Sopenharmony_ci print('Cannot fix %s: %s' % (path, err)) 2137db96d56Sopenharmony_ci return fixed 2147db96d56Sopenharmony_ci 2157db96d56Sopenharmony_ci 2167db96d56Sopenharmony_ci@status("Docs modified", modal=True) 2177db96d56Sopenharmony_cidef docs_modified(file_paths): 2187db96d56Sopenharmony_ci """Report if any file in the Doc directory has been changed.""" 2197db96d56Sopenharmony_ci return bool(file_paths) 2207db96d56Sopenharmony_ci 2217db96d56Sopenharmony_ci 2227db96d56Sopenharmony_ci@status("Misc/ACKS updated", modal=True) 2237db96d56Sopenharmony_cidef credit_given(file_paths): 2247db96d56Sopenharmony_ci """Check if Misc/ACKS has been changed.""" 2257db96d56Sopenharmony_ci return os.path.join('Misc', 'ACKS') in file_paths 2267db96d56Sopenharmony_ci 2277db96d56Sopenharmony_ci 2287db96d56Sopenharmony_ci@status("Misc/NEWS.d updated with `blurb`", modal=True) 2297db96d56Sopenharmony_cidef reported_news(file_paths): 2307db96d56Sopenharmony_ci """Check if Misc/NEWS.d has been changed.""" 2317db96d56Sopenharmony_ci return any(p.startswith(os.path.join('Misc', 'NEWS.d', 'next')) 2327db96d56Sopenharmony_ci for p in file_paths) 2337db96d56Sopenharmony_ci 2347db96d56Sopenharmony_ci@status("configure regenerated", modal=True, info=str) 2357db96d56Sopenharmony_cidef regenerated_configure(file_paths): 2367db96d56Sopenharmony_ci """Check if configure has been regenerated.""" 2377db96d56Sopenharmony_ci if 'configure.ac' in file_paths: 2387db96d56Sopenharmony_ci return "yes" if 'configure' in file_paths else "no" 2397db96d56Sopenharmony_ci else: 2407db96d56Sopenharmony_ci return "not needed" 2417db96d56Sopenharmony_ci 2427db96d56Sopenharmony_ci@status("pyconfig.h.in regenerated", modal=True, info=str) 2437db96d56Sopenharmony_cidef regenerated_pyconfig_h_in(file_paths): 2447db96d56Sopenharmony_ci """Check if pyconfig.h.in has been regenerated.""" 2457db96d56Sopenharmony_ci if 'configure.ac' in file_paths: 2467db96d56Sopenharmony_ci return "yes" if 'pyconfig.h.in' in file_paths else "no" 2477db96d56Sopenharmony_ci else: 2487db96d56Sopenharmony_ci return "not needed" 2497db96d56Sopenharmony_ci 2507db96d56Sopenharmony_cidef ci(pull_request): 2517db96d56Sopenharmony_ci if pull_request == 'false': 2527db96d56Sopenharmony_ci print('Not a pull request; skipping') 2537db96d56Sopenharmony_ci return 2547db96d56Sopenharmony_ci base_branch = get_base_branch() 2557db96d56Sopenharmony_ci file_paths = changed_files(base_branch) 2567db96d56Sopenharmony_ci python_files = [fn for fn in file_paths if fn.endswith('.py')] 2577db96d56Sopenharmony_ci c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))] 2587db96d56Sopenharmony_ci doc_files = [fn for fn in file_paths if fn.startswith('Doc') and 2597db96d56Sopenharmony_ci fn.endswith(('.rst', '.inc'))] 2607db96d56Sopenharmony_ci fixed = [] 2617db96d56Sopenharmony_ci fixed.extend(normalize_whitespace(python_files)) 2627db96d56Sopenharmony_ci fixed.extend(normalize_c_whitespace(c_files)) 2637db96d56Sopenharmony_ci fixed.extend(normalize_docs_whitespace(doc_files)) 2647db96d56Sopenharmony_ci if not fixed: 2657db96d56Sopenharmony_ci print('No whitespace issues found') 2667db96d56Sopenharmony_ci else: 2677db96d56Sopenharmony_ci print(f'Please fix the {len(fixed)} file(s) with whitespace issues') 2687db96d56Sopenharmony_ci print('(on UNIX you can run `make patchcheck` to make the fixes)') 2697db96d56Sopenharmony_ci sys.exit(1) 2707db96d56Sopenharmony_ci 2717db96d56Sopenharmony_cidef main(): 2727db96d56Sopenharmony_ci base_branch = get_base_branch() 2737db96d56Sopenharmony_ci file_paths = changed_files(base_branch) 2747db96d56Sopenharmony_ci python_files = [fn for fn in file_paths if fn.endswith('.py')] 2757db96d56Sopenharmony_ci c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))] 2767db96d56Sopenharmony_ci doc_files = [fn for fn in file_paths if fn.startswith('Doc') and 2777db96d56Sopenharmony_ci fn.endswith(('.rst', '.inc'))] 2787db96d56Sopenharmony_ci misc_files = {p for p in file_paths if p.startswith('Misc')} 2797db96d56Sopenharmony_ci # PEP 8 whitespace rules enforcement. 2807db96d56Sopenharmony_ci normalize_whitespace(python_files) 2817db96d56Sopenharmony_ci # C rules enforcement. 2827db96d56Sopenharmony_ci normalize_c_whitespace(c_files) 2837db96d56Sopenharmony_ci # Doc whitespace enforcement. 2847db96d56Sopenharmony_ci normalize_docs_whitespace(doc_files) 2857db96d56Sopenharmony_ci # Docs updated. 2867db96d56Sopenharmony_ci docs_modified(doc_files) 2877db96d56Sopenharmony_ci # Misc/ACKS changed. 2887db96d56Sopenharmony_ci credit_given(misc_files) 2897db96d56Sopenharmony_ci # Misc/NEWS changed. 2907db96d56Sopenharmony_ci reported_news(misc_files) 2917db96d56Sopenharmony_ci # Regenerated configure, if necessary. 2927db96d56Sopenharmony_ci regenerated_configure(file_paths) 2937db96d56Sopenharmony_ci # Regenerated pyconfig.h.in, if necessary. 2947db96d56Sopenharmony_ci regenerated_pyconfig_h_in(file_paths) 2957db96d56Sopenharmony_ci 2967db96d56Sopenharmony_ci # Test suite run and passed. 2977db96d56Sopenharmony_ci if python_files or c_files: 2987db96d56Sopenharmony_ci end = " and check for refleaks?" if c_files else "?" 2997db96d56Sopenharmony_ci print() 3007db96d56Sopenharmony_ci print("Did you run the test suite" + end) 3017db96d56Sopenharmony_ci 3027db96d56Sopenharmony_ci 3037db96d56Sopenharmony_ciif __name__ == '__main__': 3047db96d56Sopenharmony_ci import argparse 3057db96d56Sopenharmony_ci parser = argparse.ArgumentParser(description=__doc__) 3067db96d56Sopenharmony_ci parser.add_argument('--ci', 3077db96d56Sopenharmony_ci help='Perform pass/fail checks') 3087db96d56Sopenharmony_ci args = parser.parse_args() 3097db96d56Sopenharmony_ci if args.ci: 3107db96d56Sopenharmony_ci ci(args.ci) 3117db96d56Sopenharmony_ci else: 3127db96d56Sopenharmony_ci main() 313