1#!/usr/bin/env python3 2# Copyright 2016 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"""Wrapper to run pylint with the right settings.""" 17 18import argparse 19import errno 20import os 21import sys 22import subprocess 23from typing import Dict, List, Optional, Set 24 25 26# This script is run by repohooks users. 27# See README.md for what version we may require. 28assert (sys.version_info.major, sys.version_info.minor) >= (3, 6), ( 29 f'Python 3.6 or newer is required; found {sys.version}') 30 31 32DEFAULT_PYLINTRC_PATH = os.path.join( 33 os.path.dirname(os.path.realpath(__file__)), 'pylintrc') 34 35 36def run_lint(pylint: str, unknown: Optional[List[str]], 37 files: Optional[List[str]], init_hook: str, 38 pylintrc: Optional[str] = None) -> bool: 39 """Run lint command. 40 41 Upon error the stdout from pylint will be dumped to stdout and 42 False will be returned. 43 """ 44 cmd = [pylint] 45 46 if not files: 47 # No files to analyze for this pylintrc file. 48 return True 49 50 if pylintrc: 51 cmd += ['--rcfile', pylintrc] 52 53 files.sort() 54 cmd += unknown + files 55 56 if init_hook: 57 cmd += ['--init-hook', init_hook] 58 59 try: 60 result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True, 61 check=False) 62 except OSError as e: 63 if e.errno == errno.ENOENT: 64 print(f'{__file__}: unable to run `{cmd[0]}`: {e}', 65 file=sys.stderr) 66 print(f'{__file__}: Try installing pylint: sudo apt-get install ' 67 f'{os.path.basename(cmd[0])}', file=sys.stderr) 68 return False 69 70 raise 71 72 if result.returncode: 73 print(f'{__file__}: Using pylintrc: {pylintrc}') 74 print(result.stdout) 75 return False 76 77 return True 78 79 80def find_parent_dirs_with_pylintrc(leafdir: str, 81 pylintrc_map: Dict[str, Set[str]]) -> None: 82 """Find all dirs containing a pylintrc between root dir and leafdir.""" 83 84 # Find all pylintrc files, store the path. The path must end with '/' 85 # to make sure that string compare can be used to compare with full 86 # path to python files later. 87 88 rootdir = os.path.abspath(".") + os.sep 89 key = os.path.abspath(leafdir) + os.sep 90 91 if not key.startswith(rootdir): 92 sys.exit(f'{__file__}: The search directory {key} is outside the ' 93 f'repo dir {rootdir}') 94 95 while rootdir != key: 96 # This subdirectory has already been handled, skip it. 97 if key in pylintrc_map: 98 break 99 100 if os.path.exists(os.path.join(key, 'pylintrc')): 101 pylintrc_map.setdefault(key, set()) 102 break 103 104 # Go up one directory. 105 key = os.path.abspath(os.path.join(key, os.pardir)) + os.sep 106 107 108def map_pyfiles_to_pylintrc(files: List[str]) -> Dict[str, Set[str]]: 109 """ Map all python files to a pylintrc file. 110 111 Generate dictionary with pylintrc-file dirnames (including trailing /) 112 as key containing sets with corresponding python files. 113 """ 114 115 pylintrc_map = {} 116 # We assume pylint is running in the top directory of the project, 117 # so load the pylintrc file from there if it is available. 118 pylintrc = os.path.abspath('pylintrc') 119 if not os.path.exists(pylintrc): 120 pylintrc = DEFAULT_PYLINTRC_PATH 121 # If we pass a non-existent rcfile to pylint, it'll happily ignore 122 # it. 123 assert os.path.exists(pylintrc), f'Could not find {pylintrc}' 124 # Always add top directory, either there is a pylintrc or fallback to 125 # default. 126 key = os.path.abspath('.') + os.sep 127 pylintrc_map[key] = set() 128 129 search_dirs = {os.path.dirname(x) for x in files} 130 for search_dir in search_dirs: 131 find_parent_dirs_with_pylintrc(search_dir, pylintrc_map) 132 133 # List of directories where pylintrc files are stored, most 134 # specific path first. 135 rc_dir_names = sorted(pylintrc_map, reverse=True) 136 # Map all python files to a pylintrc file. 137 for f in files: 138 f_full = os.path.abspath(f) 139 for rc_dir in rc_dir_names: 140 # The pylintrc map keys always have trailing /. 141 if f_full.startswith(rc_dir): 142 pylintrc_map[rc_dir].add(f) 143 break 144 else: 145 sys.exit(f'{__file__}: Failed to map file {f} to a pylintrc file.') 146 147 return pylintrc_map 148 149 150def get_parser(): 151 """Return a command line parser.""" 152 parser = argparse.ArgumentParser(description=__doc__) 153 parser.add_argument('--init-hook', help='Init hook commands to run.') 154 parser.add_argument('--executable-path', default='pylint', 155 help='The path of the pylint executable.') 156 parser.add_argument('--no-rcfile', dest='use_default_conf', 157 help='Specify to use the executable\'s default ' 158 'configuration.', 159 action='store_true') 160 parser.add_argument('files', nargs='+') 161 return parser 162 163 164def main(argv): 165 """The main entry.""" 166 parser = get_parser() 167 opts, unknown = parser.parse_known_args(argv) 168 ret = 0 169 170 pylint = opts.executable_path 171 if not opts.use_default_conf: 172 pylintrc_map = map_pyfiles_to_pylintrc(opts.files) 173 first = True 174 for rc_dir, files in sorted(pylintrc_map.items()): 175 pylintrc = os.path.join(rc_dir, 'pylintrc') 176 if first: 177 first = False 178 assert os.path.abspath(rc_dir) == os.path.abspath('.'), ( 179 f'{__file__}: pylintrc in top dir not first in list') 180 if not os.path.exists(pylintrc): 181 pylintrc = DEFAULT_PYLINTRC_PATH 182 if not run_lint(pylint, unknown, sorted(files), 183 opts.init_hook, pylintrc): 184 ret = 1 185 # Not using rc files, pylint default behaviour. 186 elif not run_lint(pylint, unknown, sorted(opts.files), opts.init_hook): 187 ret = 1 188 189 return ret 190 191 192if __name__ == '__main__': 193 sys.exit(main(sys.argv[1:])) 194