Format imports
2 files changed
tree: 5feed89123699304a00f62bc5a417556c12eb5c4
  1. .bcr/
  2. .github/
  3. examples/
  4. mypy/
  5. .bazelignore
  6. .bazelrc
  7. .bazelversion
  8. .gitattributes
  9. .gitignore
  10. BUILD.bazel
  11. LICENSE
  12. MODULE.bazel
  13. readme.md
  14. WORKSPACE.bazel
readme.md

rules_mypy

Bazel rules to decorate py_* targets with mypy type-checking.

Compared to bazel-mypy-integration, this ruleset aims to make a couple of improvements:

  • Propagation of the mypy cache between dependencies within a repository to avoid exponential type-checking work
  • Robust (and automated) support for including 3rd party types/stubs packages

To propagate the mypy cache between targets, this ruleset uses build actions, which comes with a couple of trade-offs compared to bazel-mypy-integration:

  • Compared to running as an aspect, the targets produced by these rules will not run automatically when building the primary target, which may create usability trouble in some developer cycles
  • Compared to running as a test, the targets produced by these rules can fail a broad build phase, which may be undesirable in some setups

We should note that the community might prefer to treat mypy semantically as a test rather than a build action, and these rules do not enable that.

Instead, we take the opinion that type-checking is a build-time action, and the actions that are executed here take as input source files and as output produce mypy caches.

[!WARNING]
rules_mypy's build actions produce mypy caches as outputs, and these may contain large file counts and that will only grow as a dependency chain grows. This may have an impact on the size and usage of build and/or remote caches.

Usage

Whenever you define a py_binary, py_library or py_test using the rules_mypy decorated forms, rules_mypy defines a sibling target [name].mypy. Building this target will type-check the sources in [name] and leverage the mypy cache from upstream internal dependencies.

Setup is significantly easier with bzlmod, we recommend and predominantly support bzlmod, though these rules should work without issue in non-bzlmod setups, albeit with more work to configure.

Bzlmod Setup

Add rules_mypy to your MODULE.bazel:

bazel_dep(name = "rules_mypy", version = "0.0.0")

Optionally, configure a types repository:

Many Python packages have separately published types/stubs packages. While mypy (and these rules) will work without including these types, this ruleset provides some utilities for leveraging these types to improve mypy's type checking.

types = use_extension("@rules_mypy//mypy:types.bzl", "types")
types.requirements(
    name = "pip_types",
    # `@pip` in the next line corresponds to the `hub_name` when using
    # rules_python's `pip.parse(...)`.
    pip_requirements = "@pip//:requirements.bzl",
    # also legal to pass a `requirements.in` here
    requirements_txt = "//:requirements.txt",
)
use_repo(types, "pip_types")

Wrap py_* rules/macros.

If you do not already wrap py_* rules with a macro, create a .bzl file to wrap these rules:

"Custom py_* macros that also run mypy."

load("@pip_types//:types.bzl", "types")
load("@rules_mypy//mypy:mypy.bzl", "decorate")
load("@rules_python//python:py_binary.bzl", rules_python_py_binary = "py_binary")
load("@rules_python//python:py_library.bzl", rules_python_py_library = "py_library")
load("@rules_python//python:py_test.bzl", rules_python_py_test = "py_test")

py_binary = decorate(
    py_target = rules_python_py_binary,
    types = types,
)

py_library = decorate(
    py_target = rules_python_py_library,
    types = types,
)

py_test = decorate(
    py_target = rules_python_py_test,
    types = types,
)

Or, if you do already wrap py_* rules with a macro, wrap your customized rules/macros or the input py_* rules as illustrated above.

If you're using Gazelle, you may need to adjust the imports Gazelle uses for py_* targets, refer to the rules_python docs for how to do this.

Customizing mypy

mypy's behavior may be customized using a mypy config file file. To use a mypy config file, pass a label for a valid config file to the decorate method:

py_library = decorate(
    py_target = rules_python_py_library,
    mypy_ini = "//:mypy.ini",
    types = types,
)

To customize the version of mypy, use rules_python's requirements resolution and construct a custom mypy CLI:

load("@pip//:requirements.bzl", "requirements") # '@pip' must match configured pip hub_name
load("@rules_mypy//mypy:mypy.bzl", "decorate", "mypy_cli")

mypy_cli(
    name = "mypy_cli",
    mypy_requirement = requirement("mypy"),
)

py_library = decorate(
    py_target = rules_python_py_library,
    mypy_cli = ":mypy_cli",
    types = types,
)

Further, to use mypy plugins referenced in any config file, use the deps attribute of mypy_cli:

load("@pip//:requirements.bzl", "requirements") # '@pip' must match configured pip hub_name
load("@rules_mypy//mypy:mypy.bzl", "decorate", "mypy_cli")

mypy_cli(
    name = "mypy_cli",
    mypy_requirement = requirement("mypy"),
    deps = [
        requirement("pydantic"),
    ],
)

py_library = decorate(
    py_target = rules_python_py_library,
    mypy_cli = ":mypy_cli",
    types = types,
)