diff --git a/Tools/patchcheck/patchcheck.py b/Tools/patchcheck/patchcheck.py index afd010a52544a3..6d09249c136378 100755 --- a/Tools/patchcheck/patchcheck.py +++ b/Tools/patchcheck/patchcheck.py @@ -1,32 +1,35 @@ #!/usr/bin/env python3 """Check proposed changes for common issues.""" + import sys import os.path import subprocess import sysconfig +from typing import Callable, Optional -def get_python_source_dir(): - src_dir = sysconfig.get_config_var('abs_srcdir') +def get_python_source_dir() -> str: + src_dir = sysconfig.get_config_var("abs_srcdir") if not src_dir: - src_dir = sysconfig.get_config_var('srcdir') + src_dir = sysconfig.get_config_var("srcdir") return os.path.abspath(src_dir) SRCDIR = get_python_source_dir() -def n_files_str(count): +def n_files_str(count: int) -> str: """Return 'N file(s)' with the proper plurality on 'file'.""" s = "s" if count != 1 else "" return f"{count} file{s}" -def status(message, modal=False, info=None): +def status(message: str, modal: bool = False, info: Optional[Callable] = None): """Decorator to output status info to stdout.""" + def decorated_fxn(fxn): def call_fxn(*args, **kwargs): - sys.stdout.write(message + ' ... ') + sys.stdout.write(message + " ... ") sys.stdout.flush() result = fxn(*args, **kwargs) if not modal and not info: @@ -40,19 +43,18 @@ def call_fxn(*args, **kwargs): return decorated_fxn -def get_git_branch(): +def get_git_branch() -> Optional[str]: """Get the symbolic name for the current git branch""" cmd = "git rev-parse --abbrev-ref HEAD".split() try: - return subprocess.check_output(cmd, - stderr=subprocess.DEVNULL, - cwd=SRCDIR, - encoding='UTF-8') + return subprocess.check_output( + cmd, stderr=subprocess.DEVNULL, cwd=SRCDIR, encoding="UTF-8" + ) except subprocess.CalledProcessError: return None -def get_git_upstream_remote(): +def get_git_upstream_remote() -> str: """ Get the remote name to use for upstream branches @@ -63,14 +65,12 @@ def get_git_upstream_remote(): """ cmd = "git remote -v".split() output = subprocess.check_output( - cmd, - stderr=subprocess.DEVNULL, - cwd=SRCDIR, - encoding="UTF-8" + cmd, stderr=subprocess.DEVNULL, cwd=SRCDIR, encoding="UTF-8" ) # Filter to desired remotes, accounting for potential uppercasing filtered_remotes = { - remote.split("\t")[0].lower() for remote in output.split('\n') + remote.split("\t")[0].lower() + for remote in output.split("\n") if "python/cpython" in remote.lower() and remote.endswith("(fetch)") } if len(filtered_remotes) == 1: @@ -80,7 +80,7 @@ def get_git_upstream_remote(): if remote_name in filtered_remotes: return remote_name remotes_found = "\n".join( - {remote for remote in output.split('\n') if remote.endswith("(fetch)")} + {remote for remote in output.split("\n") if remote.endswith("(fetch)")} ) raise ValueError( f"Patchcheck was unable to find an unambiguous upstream remote, " @@ -89,23 +89,25 @@ def get_git_upstream_remote(): f"https://devguide.python.org/getting-started/" f"git-boot-camp/#cloning-a-forked-cpython-repository " f"\nRemotes found: \n{remotes_found}" - ) + ) -def get_git_remote_default_branch(remote_name): +def get_git_remote_default_branch(remote_name: str) -> Optional[str]: """Get the name of the default branch for the given remote It is typically called 'main', but may differ """ cmd = f"git remote show {remote_name}".split() env = os.environ.copy() - env['LANG'] = 'C' + env["LANG"] = "C" try: - remote_info = subprocess.check_output(cmd, - stderr=subprocess.DEVNULL, - cwd=SRCDIR, - encoding='UTF-8', - env=env) + remote_info = subprocess.check_output( + cmd, + stderr=subprocess.DEVNULL, + cwd=SRCDIR, + encoding="UTF-8", + env=env, + ) except subprocess.CalledProcessError: return None for line in remote_info.splitlines(): @@ -115,15 +117,17 @@ def get_git_remote_default_branch(remote_name): return None -@status("Getting base branch for PR", - info=lambda x: x if x is not None else "not a PR branch") -def get_base_branch(): - if not os.path.exists(os.path.join(SRCDIR, '.git')): +@status( + "Getting base branch for PR", + info=lambda x: x if x is not None else "not a PR branch", +) +def get_base_branch() -> Optional[str]: + if not os.path.exists(os.path.join(SRCDIR, ".git")): # Not a git checkout, so there's no base branch return None upstream_remote = get_git_upstream_remote() version = sys.version_info - if version.releaselevel == 'alpha': + if version.releaselevel == "alpha": base_branch = get_git_remote_default_branch(upstream_remote) else: base_branch = "{0.major}.{0.minor}".format(version) @@ -134,38 +138,40 @@ def get_base_branch(): return upstream_remote + "/" + base_branch -@status("Getting the list of files that have been added/changed", - info=lambda x: n_files_str(len(x))) -def changed_files(base_branch=None): +@status( + "Getting the list of files that have been added/changed", + info=lambda x: n_files_str(len(x)), +) +def changed_files(base_branch: Optional[str] = None) -> list[str]: """Get the list of changed or added files from git.""" - if os.path.exists(os.path.join(SRCDIR, '.git')): + if os.path.exists(os.path.join(SRCDIR, ".git")): # We just use an existence check here as: # directory = normal git checkout/clone # file = git worktree directory if base_branch: - cmd = 'git diff --name-status ' + base_branch + cmd = "git diff --name-status " + base_branch else: - cmd = 'git status --porcelain' + cmd = "git status --porcelain" filenames = [] - with subprocess.Popen(cmd.split(), - stdout=subprocess.PIPE, - cwd=SRCDIR) as st: + with subprocess.Popen( + cmd.split(), stdout=subprocess.PIPE, cwd=SRCDIR + ) as st: git_file_status, _ = st.communicate() if st.returncode != 0: - sys.exit(f'error running {cmd}') + sys.exit(f"error running {cmd}") for line in git_file_status.splitlines(): line = line.decode().rstrip() status_text, filename = line.split(maxsplit=1) status = set(status_text) # modified, added or unmerged files - if not status.intersection('MAU'): + if not status.intersection("MAU"): continue - if ' -> ' in filename: + if " -> " in filename: # file is renamed - filename = filename.split(' -> ', 2)[1].strip() + filename = filename.split(" -> ", 2)[1].strip() filenames.append(filename) else: - sys.exit('need a git checkout to get modified files') + sys.exit("need a git checkout to get modified files") return list(map(os.path.normpath, filenames)) @@ -179,21 +185,23 @@ def docs_modified(file_paths): @status("Misc/ACKS updated", modal=True) def credit_given(file_paths): """Check if Misc/ACKS has been changed.""" - return os.path.join('Misc', 'ACKS') in file_paths + return os.path.join("Misc", "ACKS") in file_paths @status("Misc/NEWS.d updated with `blurb`", modal=True) def reported_news(file_paths): """Check if Misc/NEWS.d has been changed.""" - return any(p.startswith(os.path.join('Misc', 'NEWS.d', 'next')) - for p in file_paths) + return any( + p.startswith(os.path.join("Misc", "NEWS.d", "next")) + for p in file_paths + ) @status("configure regenerated", modal=True, info=str) def regenerated_configure(file_paths): """Check if configure has been regenerated.""" - if 'configure.ac' in file_paths: - return "yes" if 'configure' in file_paths else "no" + if "configure.ac" in file_paths: + return "yes" if "configure" in file_paths else "no" else: return "not needed" @@ -201,18 +209,21 @@ def regenerated_configure(file_paths): @status("pyconfig.h.in regenerated", modal=True, info=str) def regenerated_pyconfig_h_in(file_paths): """Check if pyconfig.h.in has been regenerated.""" - if 'configure.ac' in file_paths: - return "yes" if 'pyconfig.h.in' in file_paths else "no" + if "configure.ac" in file_paths: + return "yes" if "pyconfig.h.in" in file_paths else "no" else: return "not needed" -def main(): +def main() -> None: base_branch = get_base_branch() file_paths = changed_files(base_branch) - has_doc_files = any(fn for fn in file_paths if fn.startswith('Doc') and - fn.endswith(('.rst', '.inc'))) - misc_files = {p for p in file_paths if p.startswith('Misc')} + has_doc_files = any( + fn + for fn in file_paths + if fn.startswith("Doc") and fn.endswith((".rst", ".inc")) + ) + misc_files = {p for p in file_paths if p.startswith("Misc")} # Docs updated. docs_modified(has_doc_files) # Misc/ACKS changed. @@ -225,8 +236,8 @@ def main(): regenerated_pyconfig_h_in(file_paths) # Test suite run and passed. - has_c_files = any(fn for fn in file_paths if fn.endswith(('.c', '.h'))) - has_python_files = any(fn for fn in file_paths if fn.endswith('.py')) + has_c_files = any(fn for fn in file_paths if fn.endswith((".c", ".h"))) + has_python_files = any(fn for fn in file_paths if fn.endswith(".py")) print() if has_c_files: print("Did you run the test suite and check for refleaks?") @@ -234,5 +245,5 @@ def main(): print("Did you run the test suite?") -if __name__ == '__main__': +if __name__ == "__main__": main()