README.md
1# Python Gazelle plugin
2
3[Gazelle](https://github.com/bazelbuild/bazel-gazelle)
4is a build file generator for Bazel projects. It can create new BUILD.bazel files for a project that follows language conventions, and it can update existing build files to include new sources, dependencies, and options.
5
6Gazelle may be run by Bazel using the gazelle rule, or it may be installed and run as a command line tool.
7
8This directory contains a plugin for
9[Gazelle](https://github.com/bazelbuild/bazel-gazelle)
10that generates BUILD files content for Python code.
11
12The following instructions are for when you use [bzlmod](https://docs.bazel.build/versions/5.0.0/bzlmod.html).
13Please refer to older documentation that includes instructions on how to use Gazelle
14without using bzlmod as your dependency manager.
15
16## Example
17
18We have an example of using Gazelle with Python located [here](https://github.com/bazelbuild/rules_python/tree/main/examples/bzlmod).
19A fully-working example without using bzlmod is in [`examples/build_file_generation`](../examples/build_file_generation).
20
21The following documentation covers using bzlmod.
22
23## Adding Gazelle to your project
24
25First, you'll need to add Gazelle to your `MODULES.bazel` file.
26Get the current version of Gazelle from there releases here: https://github.com/bazelbuild/bazel-gazelle/releases/.
27
28
29See the installation `MODULE.bazel` snippet on the Releases page:
30https://github.com/bazelbuild/rules_python/releases in order to configure rules_python.
31
32You will also need to add the `bazel_dep` for configuration for `rules_python_gazelle_plugin`.
33
34Here is a snippet of a `MODULE.bazel` file.
35
36```starlark
37# The following stanza defines the dependency rules_python.
38bazel_dep(name = "rules_python", version = "0.22.0")
39
40# The following stanza defines the dependency rules_python_gazelle_plugin.
41# For typical setups you set the version.
42bazel_dep(name = "rules_python_gazelle_plugin", version = "0.22.0")
43
44# The following stanza defines the dependency gazelle.
45bazel_dep(name = "gazelle", version = "0.31.0", repo_name = "bazel_gazelle")
46
47# Import the python repositories generated by the given module extension into the scope of the current module.
48use_repo(python, "python3_9")
49use_repo(python, "python3_9_toolchains")
50
51# Register an already-defined toolchain so that Bazel can use it during toolchain resolution.
52register_toolchains(
53 "@python3_9_toolchains//:all",
54)
55
56# Use the pip extension
57pip = use_extension("@rules_python//python:extensions.bzl", "pip")
58
59# Use the extension to call the `pip_repository` rule that invokes `pip`, with `incremental` set.
60# Accepts a locked/compiled requirements file and installs the dependencies listed within.
61# Those dependencies become available in a generated `requirements.bzl` file.
62# You can instead check this `requirements.bzl` file into your repo.
63# Because this project has different requirements for windows vs other
64# operating systems, we have requirements for each.
65pip.parse(
66 name = "pip",
67 # When using gazelle you must use set the following flag
68 # in order for the generation of gazelle dependency resolution.
69 incompatible_generate_aliases = True,
70 requirements_lock = "//:requirements_lock.txt",
71 requirements_windows = "//:requirements_windows.txt",
72)
73
74# Imports the pip toolchain generated by the given module extension into the scope of the current module.
75use_repo(pip, "pip")
76```
77Next, we'll fetch metadata about your Python dependencies, so that gazelle can
78determine which package a given import statement comes from. This is provided
79by the `modules_mapping` rule. We'll make a target for consuming this
80`modules_mapping`, and writing it as a manifest file for Gazelle to read.
81This is checked into the repo for speed, as it takes some time to calculate
82in a large monorepo.
83
84Gazelle will walk up the filesystem from a Python file to find this metadata,
85looking for a file called `gazelle_python.yaml` in an ancestor folder of the Python code.
86Create an empty file with this name. It might be next to your `requirements.txt` file.
87(You can just use `touch` at this point, it just needs to exist.)
88
89To keep the metadata updated, put this in your `BUILD.bazel` file next to `gazelle_python.yaml`:
90
91```starlark
92load("@pip//:requirements.bzl", "all_whl_requirements")
93load("@rules_python_gazelle_plugin//manifest:defs.bzl", "gazelle_python_manifest")
94load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping")
95
96# This rule fetches the metadata for python packages we depend on. That data is
97# required for the gazelle_python_manifest rule to update our manifest file.
98modules_mapping(
99 name = "modules_map",
100 wheels = all_whl_requirements,
101)
102
103# Gazelle python extension needs a manifest file mapping from
104# an import to the installed package that provides it.
105# This macro produces two targets:
106# - //:gazelle_python_manifest.update can be used with `bazel run`
107# to recalculate the manifest
108# - //:gazelle_python_manifest.test is a test target ensuring that
109# the manifest doesn't need to be updated
110gazelle_python_manifest(
111 name = "gazelle_python_manifest",
112 modules_mapping = ":modules_map",
113 # This is what we called our `pip_install` rule, where third-party
114 # python libraries are loaded in BUILD files.
115 pip_repository_name = "pip",
116 # This should point to wherever we declare our python dependencies
117 # (the same as what we passed to the modules_mapping rule in WORKSPACE)
118 requirements = "//:requirements_lock.txt",
119 # NOTE: we can use this flag in order to make our setup compatible with
120 # bzlmod.
121 use_pip_repository_aliases = True,
122)
123```
124
125Finally, you create a target that you'll invoke to run the Gazelle tool
126with the rules_python extension included. This typically goes in your root
127`/BUILD.bazel` file:
128
129```starlark
130load("@bazel_gazelle//:def.bzl", "gazelle")
131load("@rules_python_gazelle_plugin//:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
132
133# Our gazelle target points to the python gazelle binary.
134# This is the simple case where we only need one language supported.
135# If you also had proto, go, or other gazelle-supported languages,
136# you would also need a gazelle_binary rule.
137# See https://github.com/bazelbuild/bazel-gazelle/blob/master/extend.rst#example
138gazelle(
139 name = "gazelle",
140 data = GAZELLE_PYTHON_RUNTIME_DEPS,
141 gazelle = "@rules_python_gazelle_plugin//python:gazelle_binary",
142)
143```
144
145That's it, now you can finally run `bazel run //:gazelle` anytime
146you edit Python code, and it should update your `BUILD` files correctly.
147
148## Usage
149
150Gazelle is non-destructive.
151It will try to leave your edits to BUILD files alone, only making updates to `py_*` targets.
152However it will remove dependencies that appear to be unused, so it's a
153good idea to check in your work before running Gazelle so you can easily
154revert any changes it made.
155
156The rules_python extension assumes some conventions about your Python code.
157These are noted below, and might require changes to your existing code.
158
159Note that the `gazelle` program has multiple commands. At present, only the `update` command (the default) does anything for Python code.
160
161### Directives
162
163You can configure the extension using directives, just like for other
164languages. These are just comments in the `BUILD.bazel` file which
165govern behavior of the extension when processing files under that
166folder.
167
168See https://github.com/bazelbuild/bazel-gazelle#directives
169for some general directives that may be useful.
170In particular, the `resolve` directive is language-specific
171and can be used with Python.
172Examples of these directives in use can be found in the
173/gazelle/testdata folder in the rules_python repo.
174
175Python-specific directives are as follows:
176
177| **Directive** | **Default value** |
178|--------------------------------------|-------------------|
179| `# gazelle:python_extension` | `enabled` |
180| Controls whether the Python extension is enabled or not. Sub-packages inherit this value. Can be either "enabled" or "disabled". | |
181| `# gazelle:python_root` | n/a |
182| Sets a Bazel package as a Python root. This is used on monorepos with multiple Python projects that don't share the top-level of the workspace as the root. | |
183| `# gazelle:python_manifest_file_name`| `gazelle_python.yaml` |
184| Overrides the default manifest file name. | |
185| `# gazelle:python_ignore_files` | n/a |
186| Controls the files which are ignored from the generated targets. | |
187| `# gazelle:python_ignore_dependencies`| n/a |
188| Controls the ignored dependencies from the generated targets. | |
189| `# gazelle:python_validate_import_statements`| `true` |
190| Controls whether the Python import statements should be validated. Can be "true" or "false" | |
191| `# gazelle:python_generation_mode`| `package` |
192| Controls the target generation mode. Can be "package" or "project" | |
193| `# gazelle:python_library_naming_convention`| `$package_name$` |
194| Controls the `py_library` naming convention. It interpolates $package_name$ with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a generated target named `foo_my_lib`. | |
195| `# gazelle:python_binary_naming_convention` | `$package_name$_bin` |
196| Controls the `py_binary` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. | |
197| `# gazelle:python_test_naming_convention` | `$package_name$_test` |
198| Controls the `py_test` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. | |
199| `# gazelle:resolve py ...` | n/a |
200| Instructs the plugin what target to add as a dependency to satisfy a given import statement. The syntax is `# gazelle:resolve py import-string label` where `import-string` is the symbol in the python `import` statement, and `label` is the Bazel label that Gazelle should write in `deps`. | |
201
202### Libraries
203
204Python source files are those ending in `.py` but not ending in `_test.py`.
205
206First, we look for the nearest ancestor BUILD file starting from the folder
207containing the Python source file.
208
209If there is no `py_library` in this BUILD file, one is created, using the
210package name as the target's name. This makes it the default target in the
211package.
212
213Next, all source files are collected into the `srcs` of the `py_library`.
214
215Finally, the `import` statements in the source files are parsed, and
216dependencies are added to the `deps` attribute.
217
218### Unit Tests
219
220A `py_test` target is added to the BUILD file when gazelle encounters
221a file named `__test__.py`.
222Often, Python unit test files are named with the suffix `_test`.
223For example, if we had a folder that is a package named "foo" we could have a Python file named `foo_test.py`
224and gazelle would create a `py_test` block for the file.
225
226The following is an example of a `py_test` target that gazelle would add when
227it encounters a file named `__test__.py`.
228
229```starlark
230py_test(
231 name = "build_file_generation_test",
232 srcs = ["__test__.py"],
233 main = "__test__.py",
234 deps = [":build_file_generation"],
235)
236```
237
238You can control the naming convention for test targets by adding a gazelle directive named
239`# gazelle:python_test_naming_convention`. See the instructions in the section above that
240covers directives.
241
242### Binaries
243
244When a `__main__.py` file is encountered, this indicates the entry point
245of a Python program.
246
247A `py_binary` target will be created, named `[package]_bin`.
248
249## Developer Notes
250
251Gazelle extensions are written in Go. This gazelle plugin is a hybrid, as it uses Go to execute a
252Python interpreter as a subprocess to parse Python source files.
253See the gazelle documentation https://github.com/bazelbuild/bazel-gazelle/blob/master/extend.md
254for more information on extending Gazelle.
255
256If you add new Go dependencies to the plugin source code, you need to "tidy" the go.mod file.
257After changing that file, run `go mod tidy` or `bazel run @go_sdk//:bin/go -- mod tidy`
258to update the go.mod and go.sum files. Then run `bazel run //:update_go_deps` to have gazelle
259add the new dependenies to the deps.bzl file. The deps.bzl file is used as defined in our /WORKSPACE
260to include the external repos Bazel loads Go dependencies from.
261
262Then after editing Go code, run `bazel run //:gazelle` to generate/update the rules in the
263BUILD.bazel files in our repo.
264