I'm using vim as my editor and the Syntastic plugin. I'm trying to understand the idiomatic way to use pylint with tools like Bazel.
pylint has an init-hook command-line parameter that can be used to dynamically manipulate sys.hook. I was thinking of writing a wrapper script to do this, but I'm unsure how to determine the right thing to pass as an "init-hook" command.
Best way I know is to run pylint
as part of a test.
Ideally you'd have a linting rule for each file, so that when that file changes you'd only re-lint that one file. This is probably impractical though.
The other end of the scale is to have a single rule to lint all the files in your project. That rule would re-lint all files even if only one file changes. So this is inefficient.
A good middle ground in my opinion is one linting rule per Bazel package.
Assuming you have pylint as a binary in your workspace under @local_pylint_config//:pylint
for example, I recommend the following pattern:
sh_test(
name = "lint_test",
srcs = ["lint_test.sh"], # this doesn't have to do anything
data = ["lint_files.out"],
)
genrule(
name = "lint_files",
srcs = glob(["**/*.py"]),
outs = ["lint_files.out"],
tools = ["@local_pylint_config//:pylint"],
cmd = "$(location @local_pylint_config//:pylint) $(SRCS) >&/dev/null && md5sum $$(echo $(SRCS) | sort) > $@",
)
Notes:
/dev/null
to reduce build noise.md5sum
I merely touch
'ed the output file, the output's contents would be independent of the sources' content, so the downstream test rule wouldn't rerun.... && date > $@
instead of checksumming the sources would be good enough too, because Bazel would rebuild the genrule (and thus re-lint the source files) if any of the source files changed, producing a different output because by then the current time would have changed. Using a checksum however is deterministic.You can create py_test call that call a python file, that it self warp a call to pylint
or to pytest --pylint
. And to have something more reusable across the workspace create a macro around the py_test. I explain the detailed solution in Experimentations on Bazel: Python (3), linter & pytest, with link to source code.
Create the python tool (wrapp call to pytest, or only pylint) in tools/pytest/pytest_wrapper.py
import sys
import pytest
# if using 'bazel test ...'
if __name__ == "__main__":
sys.exit(pytest.main(sys.argv[1:]))
Create the macro in tools/pytest/defs.bzl
"""Wrap pytest"""
load("@rules_python//python:defs.bzl", "py_test")
load("@my_python_deps//:requirements.bzl", "requirement")
def pytest_test(name, srcs, deps = [], args = [], data = [], **kwargs):
"""
Call pytest
"""
py_test(
name = name,
srcs = [
"//tools/pytest:pytest_wrapper.py",
] + srcs,
main = "//tools/pytest:pytest_wrapper.py",
args = [
"--capture=no",
"--black",
"--pylint",
"--pylint-rcfile=$(location //tools/pytest:.pylintrc)",
# "--mypy",
] + args + ["$(location :%s)" % x for x in srcs],
python_version = "PY3",
srcs_version = "PY3",
deps = deps + [
requirement("pytest"),
requirement("pytest-black"),
requirement("pytest-pylint"),
# requirement("pytest-mypy"),
],
data = [
"//tools/pytest:.pylintrc",
] + data,
**kwargs
)
expose some resources from tools/pytest/BUILD.bazel
exports_files([
"pytest_wrapper.py",
".pylintrc",
])
Call it from your package BUILD.bazel
load("//tools/pytest:defs.bzl", "pytest_test")
...
pytest_test(
name = "test",
srcs = glob(["*.py"]),
deps = [
...
],
)
then call bazel test //...
pylint, pytest, back,... are part of the test flow
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With