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