1#!/usr/bin/env python3 2# Copyright 2024, The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Preupload hook to perform necessary checks and formatting on python files.""" 17 18import argparse 19import concurrent.futures 20import multiprocessing 21import pathlib 22import shlex 23import subprocess 24import sys 25 26 27def _filter_python_files(files: list[pathlib.Path]) -> list[pathlib.Path]: 28 """Filter a list of files and return a new list of python files only.""" 29 return [file for file in files if file.suffix == '.py'] 30 31 32def _check_run_shell_command(cmd: str, cwd: str = None) -> None: 33 """Run a shell command and raise error if failed.""" 34 if subprocess.run(shlex.split(cmd), cwd=cwd, check=False).returncode: 35 print('Preupload files did not pass Asuite preupload hook script.') 36 sys.exit(1) 37 38 39def _run_python_lint(lint_bin: str, files: list[pathlib.Path]) -> None: 40 """Run python lint binary on python files.""" 41 run_lint_on_file = lambda file: subprocess.run( 42 shlex.split(f'{lint_bin} {file.as_posix()}'), 43 check=False, 44 capture_output=True, 45 ) 46 47 cpu_count = multiprocessing.cpu_count() 48 with concurrent.futures.ThreadPoolExecutor(max_workers=cpu_count) as executor: 49 completed_processes = executor.map( 50 run_lint_on_file, _filter_python_files(files) 51 ) 52 53 has_format_issue = False 54 for process in completed_processes: 55 if not process.returncode: 56 continue 57 print(process.stdout.decode()) 58 has_format_issue = True 59 60 if has_format_issue: 61 sys.exit(1) 62 63 64def _run_pylint(files: list[pathlib.Path]) -> None: 65 """Run pylint on python files.""" 66 _run_python_lint('pylint', files) 67 68 69def _run_gpylint(files: list[pathlib.Path]) -> None: 70 """Run gpylint on python files if gpylint is available.""" 71 if subprocess.run( 72 shlex.split('which gpylint'), 73 check=False, 74 ).returncode: 75 print('gpylint not available. Will use pylint instead.') 76 _run_pylint(files) 77 return 78 79 _run_python_lint('gpylint', files) 80 81 82def _run_pyformat(files: list[pathlib.Path]) -> None: 83 """Run pyformat on certain projects.""" 84 if subprocess.run( 85 shlex.split('which pyformat'), 86 check=False, 87 ).returncode: 88 print('pyformat not available. Will skip auto formatting.') 89 return 90 91 def _run_pyformat_on_file(file): 92 completed_process = subprocess.run( 93 shlex.split('pyformat --force_quote_type single ' + file.as_posix()), 94 capture_output=True, 95 check=False, 96 ) 97 98 if completed_process.stdout: 99 subprocess.run( 100 shlex.split( 101 'pyformat -i --force_quote_type single ' + file.as_posix() 102 ), 103 check=False, 104 ) 105 return True 106 return False 107 108 cpu_count = multiprocessing.cpu_count() 109 with concurrent.futures.ThreadPoolExecutor(max_workers=cpu_count) as executor: 110 need_reformat = executor.map( 111 _run_pyformat_on_file, _filter_python_files(files) 112 ) 113 114 if any(need_reformat): 115 print( 116 'Reformatting completed. Please add the modified files to git and rerun' 117 ' the repo preupload hook.' 118 ) 119 sys.exit(1) 120 121 122def get_preupload_files() -> list[pathlib.Path]: 123 """Get the list of files to be uploaded.""" 124 parser = argparse.ArgumentParser() 125 parser.add_argument('preupload_files', nargs='*', help='Files to upload.') 126 args = parser.parse_args() 127 files_to_upload = args.preupload_files 128 if not files_to_upload: 129 # When running by users directly, only consider: 130 # added(A), renamed(R) and modified(M) files 131 # and store them in files_to_upload. 132 cmd = "git status --short | egrep [ARM] | awk '{print $NF}'" 133 files_to_upload = subprocess.check_output( 134 cmd, shell=True, encoding='utf-8' 135 ).splitlines() 136 if files_to_upload: 137 print('Modified files: %s' % files_to_upload) 138 file_paths_to_upload = [ 139 pathlib.Path(file).resolve() for file in files_to_upload 140 ] 141 return [file for file in file_paths_to_upload if file.exists()] 142 143 144if __name__ == '__main__': 145 preupload_files = get_preupload_files() 146 _run_pyformat(preupload_files) 147 _run_gpylint(preupload_files) 148