Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to configure pylint with bazel

Tags:

pylint

bazel

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.

like image 540
HK_Nava Avatar asked Nov 15 '17 09:11

HK_Nava


2 Answers

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:

  • The test passes if its dependency "lint_files" can be built, which succeeds if linting succeeds. Therefore the test succeeds if and only if linting succeeds.
  • I'm using a genrule to ensure that the "pylint" rule is built for the right configuration.
  • I'm redirecting pylint's output to /dev/null to reduce build noise.
  • I'm computing the checksum of all sources and write that to the output file, in order to write a unique output that's dependent exactly on the contents of the sources and nothing else (not, say, on the current time). I'm sorting the source files to ensure the output is deterministic. If instead of using 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.
  • However, using ... && 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.
like image 197
László Avatar answered Sep 29 '22 00:09

László


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

like image 41
David Bernard Avatar answered Sep 28 '22 22:09

David Bernard