diff --git a/BUILD b/BUILD index b2b8c57..d8b9107 100644 --- a/BUILD +++ b/BUILD @@ -1,4 +1,9 @@ +load("@bazel_gazelle//:def.bzl", "gazelle") load("@buildifier_prebuilt//:rules.bzl", "buildifier") +load("@pypi//:requirements.bzl", "all_whl_requirements") +load("@rules_python//python:pip.bzl", "compile_pip_requirements") +load("@rules_python_gazelle_plugin//manifest:defs.bzl", "gazelle_python_manifest") +load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping") # Tell bazel to include some files filegroup( @@ -34,3 +39,90 @@ buildifier( lint_mode = "fix", mode = "fix", ) + +###### +# Gazelle +# +# Gazelle is an automatic BUILD(.bazel) file generator. Run via: +# bazel run //:gazelle +###### + +# Comments that start with "# gazelle:XYZ" are called *directives*. Some directives +# can and should be set here, in the same bazel package (BUILD file) that defines +# gazelle, while other directives (such as "# gazelle:python_root") should be +# defined in a BUILD file specific to that part of the folder tree. See +# src/BUILD for such an example - it's how we define that the "src" dir should +# be the root of python files and thus get added to sys.path. + +# This directive tells gazelle that our tests are named "test_foo.py" instead +# of "foo_test.py". +# gazelle:python_test_naming_convention test_$package_name$ + +# This directive tells gazelle to make a single bazel target per python file. +# The default is to make a single bazel target per python _package_). +# gazelle:python_generation_mode file + +# This directive would be used if, for example, we wanted to make pytest_test +# rules instead of py_test rules. However, this project doesn't use `pytest` +# so the directive is inactive (double #). +## gazelle:map_kind py_test pytest_test //tools/bazel:defs.bzl + +# This directive tells gazelle to use import `foobar` using the py_library +# target, rather than the py_binary target (`foobar.py` is both a library +# and an executable, so gazelle gets confuzed, saying that multiple targets +# can satisfy the "mypackage.foobar" import). +# This directive can be set multiple times. +# gazelle:resolve py mypackage.foobar //src/mypackage:foobar + +# Python's gazelle added support for the default_visibility directive in +# https://github.com/bazelbuild/rules_python/pull/1787. We can use this to make +# all targets visible by all tests. +# gazelle:python_default_visibility //$python_root:__subpackages__,//tests:__subpackages__ + +###### End Gazelle Directives ###### + +# This rule will compile the project requirements into a lock file that +# contains versions and hashes. The lock file ends up getting used when +# installing dependencies via pip. +# bazel run //:requirements.update +compile_pip_requirements( + name = "requirements", + src = "requirements.in", + requirements_txt = "requirements_lock.txt", +) + +# This rule fetches the metadata for python packages we depend on. That data is +# required for the gazelle_python_manifest rule to update our manifest file. +modules_mapping( + name = "modules_map", + wheels = all_whl_requirements, +) + +# Gazelle python extension needs a manifest file mapping from +# an import to the installed package that provides it. +# This macro produces two targets: +# bazel run //:gazelle_python_manifest.update +# bazel run //:gazelle_python_manifest.test +gazelle_python_manifest( + name = "gazelle_python_manifest", + modules_mapping = ":modules_map", + # This is what we called our `pip_parse` rule, where third-party + # python libraries are loaded in BUILD files. + pip_repository_name = "pypi", + # This should point to wherever we declare our python dependencies + # (the same as what we passed to the pip.parse rule in MODULE.bazel) + # This argument is optional. If provided, the `.test` target is very + # fast because it just has to check an integrity field. If not provided, + # the integrity field is not added to the manifest which can help avoid + # merge conflicts in large repos. + requirements = "//:requirements_lock.txt", +) + +# Make a target for running gazelle. +# bazel run //:gazelle +# or: +# bazel run //:gazelle update # Note: "update" is the arg, not part of the target +gazelle( + name = "gazelle", + gazelle = "@rules_python_gazelle_plugin//python:gazelle_binary", +) diff --git a/MODULE.bazel b/MODULE.bazel index ab1023f..223ebbb 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,6 +19,14 @@ bazel_dep(name = "bazel_skylib", version = "1.5.0") # 4. execute py_respositories() bazel_dep(name = "rules_python", version = "0.31.0") +# Use a pre-release version of rules_python so that we can access new directives. +# Once 0.32.0 is released we can remove this line. +git_override( + module_name = "rules_python", + commit = "cdc7f2f43186899b970996f0051c702c40b10ea6", + remote = "https://github.com/bazelbuild/rules_python", +) + # Install a prebuilt version of buildifier. Buildifier is a linter and autoformatter # for starlark files. # TODO: Replace with an official prebuilt version when available. This currently @@ -26,6 +34,19 @@ bazel_dep(name = "rules_python", version = "0.31.0") # https://github.com/bazelbuild/buildtools/issues/1204 bazel_dep(name = "buildifier_prebuilt", version = "6.4.0", dev_dependency = True) +# Gazelle for auto BUILD generation. See +# https://github.com/bazelbuild/rules_python/blob/main/gazelle/README.md +# First install ruleset specifc to python, then gazelle itself. +bazel_dep(name = "rules_python_gazelle_plugin", version = "0.31.0") # same version as rules_python +bazel_dep(name = "gazelle", version = "0.35.0", repo_name = "bazel_gazelle") + +# Use a pre-release version of rules_python_gazelle_plugin so that we can +# access new directives. Once 0.32.0 is released we can remove this line. +local_path_override( + module_name = "rules_python_gazelle_plugin", + path = "/c/dev/rules_python/gazelle", +) + # Initialize the python toolchain using the rules_python extension. # This is similar to the "python_register_toolchains" function in WORKSPACE. # It creates a hermetic python rather than relying on a system-installed interpreter. @@ -40,6 +61,8 @@ pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") # Configure how we fetch python dependencies via pip pip.parse( + # Use the bazel downloader for pulling pypi packages. + experimental_index_url = "https://pypi.org/simple", # This name is what gets used in other BUILD files with `load()`. hub_name = "pypi", python_version = "3.10", diff --git a/README.md b/README.md index 9e88c2f..aecd4f9 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,21 @@ bazel run //:buildifier.check [buildifier]: https://github.com/bazelbuild/buildtools/blob/master/buildifier/README.md [starlark]: https://github.com/bazelbuild/starlark + + +### Gazelle + +[Gazelle][gazelle] is a tool for autogenerating `BUILD(.bazel)` files from source +code. + +Run by calling all these, in order: + +```shell +# If any python dependencies change: +bazel run //:requirements.update +bazel run //:gazelle_python_manifest.update +# Run gazelle and generate BUILD files and targets: +bazel run //:gazelle +``` + +[gazelle]: https://github.com/bazelbuild/bazel-gazelle diff --git a/gazelle_python.yaml b/gazelle_python.yaml new file mode 100644 index 0000000..b29350b --- /dev/null +++ b/gazelle_python.yaml @@ -0,0 +1,17 @@ +# GENERATED FILE - DO NOT EDIT! +# +# To update this file, run: +# bazel run //:gazelle_python_manifest.update + +manifest: + modules_mapping: + pathspec: pathspec + pathspec.gitignore: pathspec + pathspec.pathspec: pathspec + pathspec.pattern: pathspec + pathspec.patterns: pathspec + pathspec.patterns.gitwildmatch: pathspec + pathspec.util: pathspec + pip_repository: + name: pypi +integrity: e72e5d00a84c0e367082ae476df48fdf05a88a2a91a37815e5704e755950b758 diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..6486958 --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +pathspec diff --git a/requirements_lock.txt b/requirements_lock.txt index e69de29..7f59c73 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -0,0 +1,10 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# bazel run //:requirements.update +# +pathspec==0.12.1 \ + --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ + --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via -r requirements.in diff --git a/src/mypackage/foo.py b/src/mypackage/foo.py index fbf7d51..65e7a43 100644 --- a/src/mypackage/foo.py +++ b/src/mypackage/foo.py @@ -1,4 +1,11 @@ -from .subpackage import subfoo +# N.B.: It seems like gazelle doesn't really like relative imports yet. +# So don't do `from .subpackage import subfoo` or else it won't get the dep +# tree correct. Or maybe I'm doing something wrong? +from mypackage.subpackage import subfoo + +import pathspec + +_ = pathspec def add(a: int, b: int) -> int: