blob: 92b0dd38ce25121beba1bdf5ea233fad7a4a56d8 [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Wrapper to run pylint with the right settings."""
import argparse
import errno
import os
import sys
import subprocess
from typing import Dict, List, Optional, Set
# This script is run by repohooks users.
# See README.md for what version we may require.
assert (sys.version_info.major, sys.version_info.minor) >= (
3,
6,
), f"Python 3.6 or newer is required; found {sys.version}"
DEFAULT_PYLINTRC_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "pylintrc"
)
def run_lint(
pylint: str,
unknown: Optional[List[str]],
files: Optional[List[str]],
init_hook: str,
pylintrc: Optional[str] = None,
) -> bool:
"""Run lint command.
Upon error the stdout from pylint will be dumped to stdout and
False will be returned.
"""
cmd = [pylint]
if not files:
# No files to analyze for this pylintrc file.
return True
if pylintrc:
cmd += ["--rcfile", pylintrc]
files.sort()
cmd += unknown + files
if init_hook:
cmd += ["--init-hook", init_hook]
try:
result = subprocess.run(
cmd, stdout=subprocess.PIPE, text=True, check=False
)
except OSError as e:
if e.errno == errno.ENOENT:
print(f"{__file__}: unable to run `{cmd[0]}`: {e}", file=sys.stderr)
print(
f"{__file__}: Try installing pylint: sudo apt-get install "
f"{os.path.basename(cmd[0])}",
file=sys.stderr,
)
return False
raise
if result.returncode:
print(f"{__file__}: Using pylintrc: {pylintrc}")
print(result.stdout)
return False
return True
def find_parent_dirs_with_pylintrc(
leafdir: str, pylintrc_map: Dict[str, Set[str]]
) -> None:
"""Find all dirs containing a pylintrc between root dir and leafdir."""
# Find all pylintrc files, store the path. The path must end with '/'
# to make sure that string compare can be used to compare with full
# path to python files later.
rootdir = os.path.abspath(".") + os.sep
key = os.path.abspath(leafdir) + os.sep
if not key.startswith(rootdir):
sys.exit(
f"{__file__}: The search directory {key} is outside the "
f"repo dir {rootdir}"
)
while rootdir != key:
# This subdirectory has already been handled, skip it.
if key in pylintrc_map:
break
if os.path.exists(os.path.join(key, "pylintrc")):
pylintrc_map.setdefault(key, set())
break
# Go up one directory.
key = os.path.abspath(os.path.join(key, os.pardir)) + os.sep
def map_pyfiles_to_pylintrc(files: List[str]) -> Dict[str, Set[str]]:
"""Map all python files to a pylintrc file.
Generate dictionary with pylintrc-file dirnames (including trailing /)
as key containing sets with corresponding python files.
"""
pylintrc_map = {}
# We assume pylint is running in the top directory of the project,
# so load the pylintrc file from there if it is available.
pylintrc = os.path.abspath("pylintrc")
if not os.path.exists(pylintrc):
pylintrc = DEFAULT_PYLINTRC_PATH
# If we pass a non-existent rcfile to pylint, it'll happily ignore
# it.
assert os.path.exists(pylintrc), f"Could not find {pylintrc}"
# Always add top directory, either there is a pylintrc or fallback to
# default.
key = os.path.abspath(".") + os.sep
pylintrc_map[key] = set()
search_dirs = {os.path.dirname(x) for x in files}
for search_dir in search_dirs:
find_parent_dirs_with_pylintrc(search_dir, pylintrc_map)
# List of directories where pylintrc files are stored, most
# specific path first.
rc_dir_names = sorted(pylintrc_map, reverse=True)
# Map all python files to a pylintrc file.
for f in files:
f_full = os.path.abspath(f)
for rc_dir in rc_dir_names:
# The pylintrc map keys always have trailing /.
if f_full.startswith(rc_dir):
pylintrc_map[rc_dir].add(f)
break
else:
sys.exit(f"{__file__}: Failed to map file {f} to a pylintrc file.")
return pylintrc_map
def get_parser():
"""Return a command line parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--init-hook", help="Init hook commands to run.")
parser.add_argument(
"--executable-path",
default="pylint",
help="The path of the pylint executable.",
)
parser.add_argument(
"--no-rcfile",
dest="use_default_conf",
help="Specify to use the executable's default configuration.",
action="store_true",
)
parser.add_argument("files", nargs="+")
return parser
def main(argv):
"""The main entry."""
parser = get_parser()
opts, unknown = parser.parse_known_args(argv)
ret = 0
pylint = opts.executable_path
if not opts.use_default_conf:
pylintrc_map = map_pyfiles_to_pylintrc(opts.files)
first = True
for rc_dir, files in sorted(pylintrc_map.items()):
pylintrc = os.path.join(rc_dir, "pylintrc")
if first:
first = False
assert os.path.abspath(rc_dir) == os.path.abspath(
"."
), f"{__file__}: pylintrc in top dir not first in list"
if not os.path.exists(pylintrc):
pylintrc = DEFAULT_PYLINTRC_PATH
if not run_lint(
pylint, unknown, sorted(files), opts.init_hook, pylintrc
):
ret = 1
# Not using rc files, pylint default behaviour.
elif not run_lint(pylint, unknown, sorted(opts.files), opts.init_hook):
ret = 1
return ret
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))