diff --git a/examples/demo/MODULE.bazel b/examples/demo/MODULE.bazel index 8580aa6..3e91cf3 100644 --- a/examples/demo/MODULE.bazel +++ b/examples/demo/MODULE.bazel @@ -37,11 +37,3 @@ pip.parse( }, ) use_repo(pip, "pip") - -types = use_extension("@rules_mypy//mypy:types.bzl", "types") -types.requirements( - name = "pip_types", - pip_requirements = "@pip//:requirements.bzl", - requirements_txt = "//:requirements.txt", -) -use_repo(types, "pip_types") diff --git a/examples/demo/manual_stubs/explicit/BUILD.bazel b/examples/demo/manual_stubs/explicit/BUILD.bazel new file mode 100644 index 0000000..ddf1d22 --- /dev/null +++ b/examples/demo/manual_stubs/explicit/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +py_library( + name = "lib", + srcs = glob(["lib/**/*.py"]), + deps = [":foo-stubs"], +) + +py_library( + name = "foo-stubs", + imports = ["."], + pyi_srcs = glob(["foo/**/*.pyi"]), +) diff --git a/examples/demo/manual_stubs/explicit/foo/__init__.pyi b/examples/demo/manual_stubs/explicit/foo/__init__.pyi new file mode 100644 index 0000000..100bc63 --- /dev/null +++ b/examples/demo/manual_stubs/explicit/foo/__init__.pyi @@ -0,0 +1,2 @@ +class Foo: + pass diff --git a/examples/demo/manual_stubs/explicit/lib/main.py b/examples/demo/manual_stubs/explicit/lib/main.py new file mode 100644 index 0000000..03791e6 --- /dev/null +++ b/examples/demo/manual_stubs/explicit/lib/main.py @@ -0,0 +1 @@ +from foo import Foo diff --git a/examples/demo/manual_stubs/implicit/BUILD.bazel b/examples/demo/manual_stubs/implicit/BUILD.bazel new file mode 100644 index 0000000..d38ba91 --- /dev/null +++ b/examples/demo/manual_stubs/implicit/BUILD.bazel @@ -0,0 +1,19 @@ +load("@pip//:requirements.bzl", "requirement") +load("@rules_python//python:py_library.bzl", "py_library") + +py_library( + name = "lib", + srcs = glob(["lib/**/*.py"]), + deps = [ + # actual dependency here doesn't matter, we just need some dependency + # to implicitly provide stubs for; see `py.bzl` for the hook-up + requirement("six"), + ], +) + +py_library( + name = "foo-stubs", + imports = ["."], + pyi_srcs = glob(["foo/**/*.pyi"]), + visibility = ["//visibility:public"], +) diff --git a/examples/demo/manual_stubs/implicit/foo/__init__.pyi b/examples/demo/manual_stubs/implicit/foo/__init__.pyi new file mode 100644 index 0000000..100bc63 --- /dev/null +++ b/examples/demo/manual_stubs/implicit/foo/__init__.pyi @@ -0,0 +1,2 @@ +class Foo: + pass diff --git a/examples/demo/manual_stubs/implicit/lib/main.py b/examples/demo/manual_stubs/implicit/lib/main.py new file mode 100644 index 0000000..03791e6 --- /dev/null +++ b/examples/demo/manual_stubs/implicit/lib/main.py @@ -0,0 +1 @@ +from foo import Foo diff --git a/examples/demo/py.bzl b/examples/demo/py.bzl index 77ced1d..800b617 100644 --- a/examples/demo/py.bzl +++ b/examples/demo/py.bzl @@ -1,6 +1,14 @@ "Custom py_library rule that also runs mypy." -load("@pip_types//:types.bzl", "types") -load("@rules_mypy//mypy:mypy.bzl", "mypy") +load("@pip//:requirements.bzl", "all_requirements", "requirement") +load("@rules_mypy//mypy:mypy.bzl", "load_stubs", "mypy") -mypy_aspect = mypy(types = types) +stubs = load_stubs( + requirements = all_requirements, + overrides = { + # See manual_stubs/implicit/ + requirement("six"): "@@//manual_stubs/implicit:foo-stubs", + }, +) + +mypy_aspect = mypy(stubs = stubs) diff --git a/examples/demo/requirements.in b/examples/demo/requirements.in index 94782d0..47b75a3 100644 --- a/examples/demo/requirements.in +++ b/examples/demo/requirements.in @@ -1,3 +1,7 @@ -cachetools~=5.4.0 +# ----- thm/ dependencies ----- # numpy~=1.26.4 +cachetools~=5.4.0 types-cachetools~=5.4.0.20240717 + +# ---- manual_stubs/ dependencies ----- # +six==1.17.0 diff --git a/examples/demo/requirements.txt b/examples/demo/requirements.txt index ae586c9..7425dce 100644 --- a/examples/demo/requirements.txt +++ b/examples/demo/requirements.txt @@ -44,6 +44,10 @@ numpy==1.26.4 \ --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f # via -r requirements.in +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via -r requirements.in types-cachetools==5.4.0.20240717 \ --hash=sha256:1eae90c48760bac44ab89108be938e8ce1d740910f2d4b68446dcdc82763f186 \ --hash=sha256:67c84c26df988039be68344b162afd2dd7cd3741dc08e7d67aa1954782fd2d2a diff --git a/examples/opt-in/MODULE.bazel b/examples/opt-in/MODULE.bazel index b725a49..137b9e9 100644 --- a/examples/opt-in/MODULE.bazel +++ b/examples/opt-in/MODULE.bazel @@ -37,11 +37,3 @@ pip.parse( }, ) use_repo(pip, "pip") - -types = use_extension("@rules_mypy//mypy:types.bzl", "types") -types.requirements( - name = "pip_types", - pip_requirements = "@pip//:requirements.bzl", - requirements_txt = "//:requirements.txt", -) -use_repo(types, "pip_types") diff --git a/examples/opt-in/py.bzl b/examples/opt-in/py.bzl index 3de9986..7cbc4d6 100644 --- a/examples/opt-in/py.bzl +++ b/examples/opt-in/py.bzl @@ -1,10 +1,12 @@ "Custom py_library rule that also runs mypy." -load("@pip_types//:types.bzl", "types") -load("@rules_mypy//mypy:mypy.bzl", "mypy") +load("@pip//:requirements.bzl", "all_requirements") +load("@rules_mypy//mypy:mypy.bzl", "load_stubs", "mypy") + +stubs = load_stubs(requirements = all_requirements) mypy_aspect = mypy( # only run mypy on targets with the typecheck tag opt_in_tags = ["typecheck"], - types = types, + stubs = stubs, ) diff --git a/mypy/BUILD.bazel b/mypy/BUILD.bazel index 07c658a..094e476 100644 --- a/mypy/BUILD.bazel +++ b/mypy/BUILD.bazel @@ -5,15 +5,3 @@ bzl_library( srcs = ["mypy.bzl"], visibility = ["//visibility:public"], ) - -bzl_library( - name = "py_type_library", - srcs = ["py_type_library.bzl"], - visibility = ["//visibility:public"], -) - -bzl_library( - name = "types", - srcs = ["types.bzl"], - visibility = ["//visibility:public"], -) diff --git a/mypy/mypy.bzl b/mypy/mypy.bzl index e77a300..c175f39 100644 --- a/mypy/mypy.bzl +++ b/mypy/mypy.bzl @@ -1,6 +1,13 @@ "Public API for interacting with the mypy rule." -load("//mypy/private:mypy.bzl", _mypy = "mypy", _mypy_cli = "mypy_cli") +load( + "//mypy/private:mypy.bzl", + _load_stubs = "load_stubs", + _mypy = "mypy", + _mypy_cli = "mypy_cli", +) + +load_stubs = _load_stubs # re-export mypy aspect factory mypy = _mypy diff --git a/mypy/private/BUILD.bazel b/mypy/private/BUILD.bazel index db11212..378c900 100644 --- a/mypy/private/BUILD.bazel +++ b/mypy/private/BUILD.bazel @@ -1,6 +1,4 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -load("@rules_mypy_pip//:requirements.bzl", "requirement") -load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_uv//uv:pip.bzl", "pip_compile") load("@rules_uv//uv:venv.bzl", "create_venv") load(":mypy.bzl", "mypy_cli") @@ -16,18 +14,6 @@ bzl_library( visibility = ["//mypy:__subpackages__"], ) -bzl_library( - name = "py_type_library_rules", - srcs = ["py_type_library.bzl"], - visibility = ["//mypy:__subpackages__"], -) - -bzl_library( - name = "types_rules", - srcs = ["types.bzl"], - visibility = ["//mypy:__subpackages__"], -) - pip_compile( name = "generate_requirements_lock", requirements_in = "requirements.in", @@ -40,14 +26,3 @@ create_venv( ) mypy_cli(name = "mypy") - -py_binary( - name = "py_type_library", - srcs = ["py_type_library.py"], - main = "py_type_library.py", - python_version = "3.12", - visibility = ["//visibility:public"], - deps = [ - requirement("click"), - ], -) diff --git a/mypy/private/mypy.bzl b/mypy/private/mypy.bzl index b788973..d17286e 100644 --- a/mypy/private/mypy.bzl +++ b/mypy/private/mypy.bzl @@ -10,7 +10,6 @@ directories. load("@rules_mypy_pip//:requirements.bzl", "requirement") load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_info.bzl", RulesPythonPyInfo = "PyInfo") -load(":py_type_library.bzl", "PyTypeLibraryInfo") MypyCacheInfo = provider( doc = "Output details of the mypy build rule.", @@ -106,12 +105,12 @@ def _mypy_impl(target, ctx): upstream_caches = [] - types = [] + stubs_deps = [] depsets = [] - type_mapping = dict(zip([k.label for k in ctx.attr._types_keys], ctx.attr._types_values)) - dep_with_stubs = [_.label.workspace_root + "/site-packages" for _ in ctx.attr._types_keys] + type_mapping = dict(zip([k.label for k in ctx.attr._stubs_keys], ctx.attr._stubs_values)) + dep_with_stubs = [_.label.workspace_root + "/site-packages" for _ in ctx.attr._stubs_keys] additional_types = [ type_mapping[dep.label] for dep in ctx.rule.attr.deps @@ -128,10 +127,15 @@ def _mypy_impl(target, ctx): pyi_files.extend(dep[RulesPythonPyInfo].direct_pyi_files.to_list()) pyi_dirs |= {"%s/%s" % (ctx.bin_dir.path, imp): None for imp in _extract_imports(dep) if imp != "site-packages" and imp != "_main"} depsets.append(dep.default_runfiles.files) - if PyTypeLibraryInfo in dep: - types.append(dep[PyTypeLibraryInfo].directory.path + "/site-packages") - elif dep.label in type_mapping: + + if dep.label in type_mapping: continue + + if dep.label.workspace_name == "": + for import_ in _extract_imports(dep): + imports_dirs[import_] = 1 + elif dep in type_mapping.values(): + stubs_deps.append(dep.label.workspace_root + "/site-packages") elif dep.label.workspace_root.startswith("external/"): # TODO: do we need this, still? external_deps[dep.label.workspace_root + "/site-packages"] = 1 @@ -139,9 +143,6 @@ def _mypy_impl(target, ctx): path = "external/{}".format(imp) if path not in dep_with_stubs: external_deps[path] = 1 - elif dep.label.workspace_name == "": - for import_ in _extract_imports(dep): - imports_dirs[import_] = 1 if MypyCacheInfo in dep: upstream_caches.append(dep[MypyCacheInfo].directory) @@ -169,9 +170,6 @@ def _mypy_impl(target, ctx): # output bin_dir from `generated_dirs` first # https://github.com/theoremlp/rules_mypy/issues/88 ["."] + - # types need to appear first in the mypy path since the module directories - # are the same and mypy resolves the first ones, first. - sorted(types) + sorted(external_deps) + sorted(imports_dirs) + sorted(generated_dirs) + @@ -224,7 +222,7 @@ def _mypy_impl(target, ctx): outputs = outputs, executable = ctx.executable._mypy_cli, arguments = [args], - env = {"MYPYPATH": mypy_path} | ctx.configuration.default_shell_env | extra_env, + env = {"MYPYPATH": mypy_path, "PYTHONPATH": ":".join(sorted(stubs_deps))} | ctx.configuration.default_shell_env | extra_env, ) return result_info @@ -232,7 +230,7 @@ def _mypy_impl(target, ctx): def mypy( mypy_cli = None, mypy_ini = None, - types = None, + stubs = None, cache = True, color = True, suppression_tags = None, @@ -244,15 +242,7 @@ def mypy( mypy_cli: (optional) a replacement mypy_cli to use (recommended to produce with mypy_cli macro) mypy_ini: (optional) mypy_ini file to use - types: (optional) a dict of dependency label to types dependency label - example: - ``` - { - requirement("cachetools"): requirement("types-cachetools"), - } - ``` - Use the types extension to create this map for a requirements.in - or requirements.txt file. + stubs: (optional) result from load_stubs() cache: (optional, default True) propagate the mypy cache color: (optional, default True) use color in mypy output suppression_tags: (optional, default ["no-mypy"]) tags that suppress running @@ -264,8 +254,6 @@ def mypy( Returns: a mypy aspect. """ - types = types or {} - additional_attrs = {} return aspect( @@ -286,8 +274,8 @@ def mypy( ), # pass the dict[Label, Label] in parts because Bazel doesn't have # this kind of attr to pass naturally - "_types_keys": attr.label_list(default = types.keys()), - "_types_values": attr.label_list(default = types.values()), + "_stubs_keys": attr.label_list(default = stubs.mapping.keys() if stubs else []), + "_stubs_values": attr.label_list(default = stubs.mapping.values() if stubs else []), "_suppression_tags": attr.string_list(default = suppression_tags or ["no-mypy"]), "_opt_in_tags": attr.string_list(default = opt_in_tags or []), "cache": attr.bool(default = cache), @@ -295,6 +283,52 @@ def mypy( } | additional_attrs, ) +def _load_stubs_from_requirements(requirements): + # for a package "foo-bar", maps "foo_bar" to "@pip//foo_bar:pkg" + parsed_reqs = {} + for req in requirements: + parsed_reqs[Label(req).package] = req + + stubs = {} + for req in requirements: + req_name = Label(req).package + + if req_name.endswith("_stubs"): + base_req_name = req_name.removesuffix("_stubs") + elif req_name.startswith("types_"): + base_req_name = req_name.removeprefix("types_") + else: + continue + + if base_req_name in parsed_reqs: + base_req = parsed_reqs[base_req_name] + stubs[base_req] = req + + return stubs + +def load_stubs(requirements = [], overrides = {}): + """ + Generate a mapping of labels to their stubs label. For example: + + ``` + { + requirement("cachetools"): requirement("types-cachetools"), + } + ``` + + This can be detected automatically via `requirements`, manually via `overrides`, + or a combination of both. + + Args: + requirements: (optional) the full list of requirements from "@pip//:requirements.bzl" + to automatically detect stubs from. + overrides: (optional) explicitly specified stubs mapping + """ + stubs = _load_stubs_from_requirements(requirements) + return struct( + mapping = stubs | overrides, + ) + def mypy_cli(name, deps = None, mypy_requirement = None, python_version = "3.12", tags = None): """ Produce a custom mypy executable for use with the mypy build rule. diff --git a/mypy/private/py_type_library.bzl b/mypy/private/py_type_library.bzl deleted file mode 100644 index b763b97..0000000 --- a/mypy/private/py_type_library.bzl +++ /dev/null @@ -1,42 +0,0 @@ -"Convert pip typings packages for use with mypy." - -PyTypeLibraryInfo = provider( - doc = "Information about the content of a py_type_library.", - fields = { - "directory": "Directory containing site-packages.", - }, -) - -def _py_type_library_impl(ctx): - directory = ctx.actions.declare_directory(ctx.attr.name) - - args = ctx.actions.args() - args.add("--input-dir", ctx.attr.typing.label.workspace_root) - args.add("--output-dir", directory.path) - - ctx.actions.run( - mnemonic = "BuildPyTypeLibrary", - inputs = depset(transitive = [ctx.attr.typing.default_runfiles.files]), - outputs = [directory], - executable = ctx.executable._exec, - arguments = [args], - env = ctx.configuration.default_shell_env, - ) - - return [ - DefaultInfo( - files = depset([directory]), - runfiles = ctx.runfiles(files = [directory]), - ), - PyTypeLibraryInfo( - directory = directory, - ), - ] - -py_type_library = rule( - implementation = _py_type_library_impl, - attrs = { - "typing": attr.label(), - "_exec": attr.label(cfg = "exec", default = "//mypy/private:py_type_library", executable = True), - }, -) diff --git a/mypy/private/py_type_library.py b/mypy/private/py_type_library.py deleted file mode 100644 index 8648ac8..0000000 --- a/mypy/private/py_type_library.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -py_type_library CLI. - -Many of the typings/stubs published to pypi for package [x] show up use -[x]-stubs as their base-package. mypy expects the typings/stubs to exist -in the as-used-in-Python package [x] when placed on the path. This CLI -creates a copy of the input directory's site-packages content dropping -`-stubs` from any directory in the site-packages dir while copying. -""" - -import pathlib -import shutil - -import click - - -def _clean(package: str) -> str: - return package.removesuffix("-stubs") - - -@click.command() -@click.option("--input-dir", required=True, type=click.Path(exists=True)) -@click.option("--output-dir", required=True, type=click.Path()) -def main(input_dir: str, output_dir: str) -> None: - input = pathlib.Path(input_dir) / "site-packages" - - output = pathlib.Path(output_dir) / "site-packages" - output.mkdir(parents=True, exist_ok=True) - - for package in input.iterdir(): - shutil.copytree(input / package.name, output / _clean(package.name)) - - -if __name__ == "__main__": - main() diff --git a/mypy/private/requirements.in b/mypy/private/requirements.in index 66d208e..9450dca 100644 --- a/mypy/private/requirements.in +++ b/mypy/private/requirements.in @@ -1,3 +1 @@ -click~=8.2.1 mypy~=1.17.1 -colorama~=0.4.6 diff --git a/mypy/private/requirements.txt b/mypy/private/requirements.txt index 796e861..19eef69 100644 --- a/mypy/private/requirements.txt +++ b/mypy/private/requirements.txt @@ -2,14 +2,6 @@ # bazel run @@//mypy/private:generate_requirements_lock --index-url https://pypi.org/simple -click==8.2.1 \ - --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ - --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b - # via -r mypy/private/requirements.in -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via -r mypy/private/requirements.in mypy==1.17.1 \ --hash=sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341 \ --hash=sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5 \ diff --git a/mypy/private/types.bzl b/mypy/private/types.bzl deleted file mode 100644 index 7a58694..0000000 --- a/mypy/private/types.bzl +++ /dev/null @@ -1,78 +0,0 @@ -"Repository rule to generate `py_type_library` from input typings/stubs requirements." - -_PY_TYPE_LIBRARY_TEMPLATE = """ -py_type_library( - name = "{requirement}", - typing = requirement("{requirement}"), - visibility = ["//visibility:public"], -) -""" - -def _render_build(rctx, types): - content = "" - content += """load("{pip_requirements}", "requirement")\n""".format( - pip_requirements = rctx.attr.pip_requirements, - ) - content += """load("@rules_mypy//mypy:py_type_library.bzl", "py_type_library")\n""" - for requirement in types: - content += _PY_TYPE_LIBRARY_TEMPLATE.format( - requirement = requirement, - raw = requirement.removeprefix("types-").removesuffix("-stubs"), - ) + "\n" - return content - -def _render_types_bzl(rctx, types): - content = "" - content += """load("{pip_requirements}", "requirement")\n""".format( - pip_requirements = rctx.attr.pip_requirements, - ) - content += "types = {\n" - for requirement in types: - content += """ requirement("{raw}"): "@@{name}//:{requirement}",\n""".format( - raw = requirement.removeprefix("types-").removesuffix("-stubs"), - name = str(rctx.attr.name), - requirement = requirement, - ) - content += "}\n" - return content - -def _generate_impl(rctx): - contents = rctx.read(rctx.attr.requirements_txt) - - types = [] - - # this is a very, very naive parser - for line in contents.splitlines(): - if line.startswith("#") or line == "": - continue - - if ";" in line: - line, _ = line.split(";") - - if "~=" in line: - req, _ = line.split("~=") - elif "==" in line: - req, _ = line.split("==") - elif "<=" in line: - req, _ = line.split("<=") - else: - continue - - req = req.strip() - if req in rctx.attr.exclude_requirements: - continue - - if req.endswith("-stubs") or req.startswith("types-"): - types.append(req) - - rctx.file("BUILD.bazel", content = _render_build(rctx, types)) - rctx.file("types.bzl", content = _render_types_bzl(rctx, types)) - -generate = repository_rule( - implementation = _generate_impl, - attrs = { - "pip_requirements": attr.label(), - "requirements_txt": attr.label(allow_single_file = True), - "exclude_requirements": attr.string_list(default = []), - }, -) diff --git a/mypy/py_type_library.bzl b/mypy/py_type_library.bzl deleted file mode 100644 index 7fce65e..0000000 --- a/mypy/py_type_library.bzl +++ /dev/null @@ -1,5 +0,0 @@ -"Public API for py_type_library." - -load("//mypy/private:py_type_library.bzl", _py_type_library = "py_type_library") - -py_type_library = _py_type_library diff --git a/mypy/types.bzl b/mypy/types.bzl deleted file mode 100644 index 74e4813..0000000 --- a/mypy/types.bzl +++ /dev/null @@ -1,29 +0,0 @@ -"Extension for generating a types repository containing py_type_librarys for requirements." - -load("//mypy/private:types.bzl", "generate") - -requirements = tag_class( - attrs = { - "name": attr.string(), - "pip_requirements": attr.label(), - "requirements_txt": attr.label(mandatory = True, allow_single_file = True), - "exclude_requirements": attr.string_list(default = [], mandatory = False), - }, -) - -def _extension(module_ctx): - for mod in module_ctx.modules: - for tag in mod.tags.requirements: - generate( - name = tag.name, - pip_requirements = tag.pip_requirements, - requirements_txt = tag.requirements_txt, - exclude_requirements = tag.exclude_requirements, - ) - -types = module_extension( - implementation = _extension, - tag_classes = { - "requirements": requirements, - }, -) diff --git a/readme.md b/readme.md index 9730a11..d10d4eb 100644 --- a/readme.md +++ b/readme.md @@ -24,32 +24,19 @@ Setup is significantly easier with bzlmod, we recommend and predominantly suppor 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. - -```starlark -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") -``` - **Configure `mypy_aspect`.** Define a new aspect in a `.bzl` file (such as `./tools/aspects.bzl`): ```starlark -load("@pip_types//:types.bzl", "types") -load("@rules_mypy//mypy:mypy.bzl", "mypy") +load("@pip//:requirements.bzl", "all_requirements", "requirement") +load("@rules_mypy//mypy:mypy.bzl", "load_stubs", "mypy") + +stubs = load_stubs( + # See "Specifying stubs" +) -mypy_aspect = mypy(types = types) +mypy_aspect = mypy(stubs = stubs) ``` Update your `.bazelrc` to include this new aspect: @@ -62,6 +49,64 @@ build --aspects //tools:aspects.bzl%mypy_aspect build --output_groups=+mypy ``` +### Specifying stubs + +If a third-party library does not contain type hints, it likely has type stubs defined in a `types-somelib` or `somelib-stubs` library. If it does not, you will need to create your own [stub files](https://mypy.readthedocs.io/en/stable/stubs.html) for the dependency. + +Whether the stub files are provided by a `types-somelib` library or written yourself, Bazel needs to be told that typechecking your code depends on `types-somelib`. This can be done in one of two ways: + +1. Configure `rules_mypy` to automatically add `types-somelib` to the typechecking environment when it sees `somelib` as a dependency + * This is the recommended approach, and should be sufficient in most cases + * If any stubs libraries has dependencies (e.g. `types-boto3`), this approach will not work + +1. Add both `requirement("somelib")` and `requirement("types-somelib")` as dependencies + +The rest of this section will focus on the first approach, which is done via the `stubs` parameter to the `mypy()` aspect, which should be constructed with the `load_stubs()` helper. + +In the common case, a library has type hints provided at `types-` or `-stubs`; the `requirements` parameter to `load_stubs()` automatically detects these libraries: + +```starlark +# load the requirements.bzl file from the repo you configured with pip.parse() +load("@pip//:requirements.bzl", "all_requirements") + +stubs = load_stubs(requirements = all_requirements) +``` + +Some libraries have a different stubs library name, e.g. `grpc-stubs` is the stubs library for `grpcio`. These cases need to be manually specified: + +```starlark +load("@pip//:requirements.bzl", "requirement") + +stubs = load_stubs( + overrides = { + requirement("grpcio"): requirement("grpc-stubs"), + }, +) +``` + +If you need to write your own stubs, you can define a new `py_library` target and specify it in `load_stubs`: + +```starlark +# stubs/BUILD.bazel + +load("@rules_python//python:py_library.bzl", "py_library") + +py_library( + name = "kafka-python", + imports = ["."], + pyi_srcs = ["kafka/__init__.pyi"], + visibility = ["//visibility:public"], +) + +# aspects.bzl + +stubs = load_stubs( + overrides = { + requirement("kafka-python"): "@@//stubs:kafka-python", + }, +) +``` + ## Customizing mypy ### Configuring mypy with mypy.ini