blob: 45348e08cde51f557be2b4214fa510f91a6cfa72 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Run pylint with the right settings."""
import functools
import json
import logging
import os
from pathlib import Path
import shutil
import sys
import libdot
# URI to the official pylint docs.
MAIN_DOCS = (
"http://pylint.pycqa.org/en/latest/technical_reference/"
"features.html#pylint-checkers-options-and-switches"
)
# URI base for user managed wiki. It's sometimes better.
USER_BASE_URI = "http://pylint-messages.wikidot.com/messages:%s"
def convert_to_kokoro(data):
"""Take pylint JSON output and convert it to kokoro comment format.
The |data| input will look like:
[
{
"type": "<informational|convention|error|fatal|...>",
"module": "generate-externs",
"obj": "typename",
"line": 20,
"column": 0,
"path": "bin/generate-externs",
"symbol": "docstring-first-line-empty",
"message": "First line empty in function docstring",
"message-id": "C0199"
}
]
See eslint.convert_to_kokoro for example return value.
"""
for result in data:
msg = (
f"[pylint] {result['symbol']} ({result['message-id']})\n"
+ result["message"]
+ "\n"
+ MAIN_DOCS
+ "\n"
+ USER_BASE_URI % (result["message-id"],)
)
path = os.path.join(os.getcwd(), result["path"])
yield {
"path": os.path.relpath(path, libdot.LIBAPPS_DIR),
"message": msg,
"startLine": result["line"],
"endLine": result["line"],
"startCharacter": result["column"],
"endCharacter": result["column"],
}
def filter_known_files(paths: list[Path]) -> list[Path]:
"""Figure out what files this linter supports."""
ret = []
for path in paths:
path = Path(path)
if path.suffix == ".py":
# Add all .py files as they should only be Python.
ret += [path]
elif path.is_symlink():
# Ignore symlinks.
pass
elif not path.exists():
# Ignore missing files here (caller will handle it).
pass
elif path.is_file() and path.stat().st_mode & 0o111:
# Add executable programs with python shebangs.
with path.open("rb") as fp:
shebang = fp.readline()
if b"python" in shebang:
ret += [path]
return [str(x) for x in ret]
@functools.lru_cache(maxsize=1)
def find_pylint():
"""Figure out the name of the pylint tool."""
# Prefer our vpython copy if possible.
if shutil.which("vpython3"):
return libdot.BIN_DIR / "pylint-vpython"
# If there's no pylint, give up.
if not shutil.which("pylint"):
logging.error(
"unable to locate pylint; please install:\n"
"sudo apt-get install pylint"
)
sys.exit(1)
return "pylint"
def setup():
"""Initialize the tool settings."""
find_pylint()
def run(argv=(), pythonpaths=(), **kwargs):
"""Run the tool directly."""
setup()
# Add libdot to search path so pylint can find it. Any subproject that
# uses us will make sure it's in the search path too.
path = os.environ.get("PYTHONPATH", "")
paths = [libdot.BIN_DIR] + list(pythonpaths)
if path is not None:
paths.append(path)
extra_env = kwargs.pop("extra_env", {})
assert "PYTHONPATH" not in extra_env
kwargs["extra_env"] = {
"PYTHONPATH": os.pathsep.join(str(x) for x in paths),
**extra_env,
}
pylintrc = os.path.relpath(
os.path.join(libdot.LIBAPPS_DIR, ".pylintrc"), os.getcwd()
)
cmd = [find_pylint(), "--rcfile", pylintrc] + list(argv)
return libdot.run(cmd, **kwargs)
def perform(
argv=(), paths=(), fix=False, gerrit_comments_file=None, pythonpaths=()
):
"""Run high level tool logic."""
ret = True
argv = list(argv)
paths = list(paths)
# Pylint doesn't have any automatic fixing logic.
if fix:
return ret
comments_path = libdot.lint.kokoro_comments_path(
gerrit_comments_file, "pylint"
)
result = run(argv + paths, pythonpaths=pythonpaths, check=False)
if result.returncode:
ret = False
# Rerun for Gerrit.
if comments_path:
# Handle relative paths like "foo.json".
dirname = os.path.dirname(comments_path)
if dirname:
os.makedirs(dirname, exist_ok=True)
argv += ["--output-format=json"]
result = run(
argv + paths,
pythonpaths=pythonpaths,
check=False,
capture_output=True,
)
# Save a copy for debugging later.
with open(comments_path + ".in", "wb") as fp:
fp.write(result.stdout)
data = json.loads(result.stdout.decode("utf-8"))
comments = list(convert_to_kokoro(data))
with open(comments_path, "w", encoding="utf-8") as fp:
json.dump(comments, fp, sort_keys=True)
elif comments_path:
# If there were no failures, clear the files to avoid leaving previous
# results laying around & confuse devs.
libdot.unlink(comments_path)
libdot.unlink(comments_path + ".in")
return ret
def get_parser():
"""Get a command line parser."""
parser = libdot.ArgumentParser(description=__doc__, short_options=False)
parser.add_argument(
"--gerrit-comments-file",
help="Save errors for posting files to Gerrit.",
)
parser.add_argument("paths", nargs="*", help="Paths to lint.")
return parser
def main(argv, pythonpaths=()):
"""The main func!"""
parser = get_parser()
opts, args = parser.parse_known_args(argv)
return (
0
if perform(
argv=args,
paths=opts.paths,
pythonpaths=pythonpaths,
gerrit_comments_file=opts.gerrit_comments_file,
)
else 1
)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))