5

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.

limdor
  • 30
  • 7
HK_Nava
  • 61
  • 1
  • 5
  • I don't see the connection between Bazel and PyLint, or what you're trying to achieve. How do you envision them interacting? – László Dec 04 '17 at 15:27
  • @László Presumably: making it so that pylint is run via bazel, like other kinds of tests would be. – alecbz Jan 05 '18 at 16:07

2 Answers2

5

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.
László
  • 3,973
  • 1
  • 13
  • 26
  • Seems like a good trade-off, having a single lint result per package. Can you elaborate a little on how to: *have pylint as a binary in your workspace under @local_pylint_config//:pylint*? Also, could the `genrule` bit likely be put into something like `third_party/pylint.BUILD` file for reusablity? – hauron Sep 27 '18 at 14:34
  • Re: pylint, I meant creating a local repository with name "local_pylint_config", whose BUILD file contained a target to wrap the pylint binary. Example: in `WORKSPACE` file, add `new_local_repository(name="local_pylint_config", path="/usr/bin", build_file_content="""sh_binary(name="pylint-bin", srcs=["pylint"], visibility=["//visibility:public"])""")`. Alternatively: implement a Starlark repo rule that discovers where's pylint, instantiates a BUILD file similar to my example. – László Oct 16 '18 at 13:02
  • Normal and Starlark repo rule examples: https://stackoverflow.com/a/50017400/7778502 (FYI: "Skylark" was renamed to "Starlark") – László Oct 16 '18 at 13:04
3

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

David Bernard
  • 1,560
  • 13
  • 15