Skip to content

feat(gazelle): Gazelle plugin generates py_proto_library #3057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c85c56f
[wip] Start work on generating py_proto_library, by creating a bunch …
shaldengeki Jul 3, 2025
8b5a91b
Remove test cases that don't make any sense; py_proto_library runs on…
shaldengeki Jul 3, 2025
d8f9b1a
Add additional test fixtures required to run tests
shaldengeki Jul 3, 2025
caf5d6e
[wip] Start working on generate.go
shaldengeki Jul 3, 2025
b712696
Fix visibility in tests
shaldengeki Jul 3, 2025
3186761
Load py_proto_library from protobuf repository, since rules_proto's c…
shaldengeki Jul 3, 2025
e073055
Remove unneessary code
shaldengeki Jul 3, 2025
aa9f4aa
Delete some more unused stuff
shaldengeki Jul 3, 2025
b6df17b
Update CHANGELOG.md
shaldengeki Jul 3, 2025
d794ac1
Have a sudden change-of-heart and call it python_generate_proto, to a…
shaldengeki Jul 3, 2025
cad6849
Update README.md
shaldengeki Jul 3, 2025
a0e4f5f
Don't repeatedly initialize a variable
shaldengeki Jul 5, 2025
161cc51
Add test enforcing that unnecessary rules are removed, and change log…
shaldengeki Jul 5, 2025
fc7fa08
Add a tiny comment
shaldengeki Jul 5, 2025
ad4f3f7
Merge branch 'main' into gazelle-plugin-generates-py-proto-library
shaldengeki Jul 5, 2025
4853207
Default python_generate_proto to false, and update tests
shaldengeki Jul 5, 2025
9a44ef3
In README.md add details section
shaldengeki Jul 5, 2025
362dea9
Plugin detects configured name for protobuf directory
shaldengeki Jul 5, 2025
4e41e0e
Update Gazelle, to bring in bzlmod support
shaldengeki Jul 5, 2025
dc27cda
Update Gazelle, to bring in bzlmod support
shaldengeki Jul 5, 2025
5ee804a
Add tests covering bzlmod & renaming functionality
shaldengeki Jul 5, 2025
e5fbed8
Remove unnecessary licenses
shaldengeki Jul 5, 2025
a2cb993
Add trailing newlines to some protobuf
shaldengeki Jul 5, 2025
7c2706c
Add test covering "deletes only the py_proto_library rule"
shaldengeki Jul 5, 2025
59561fc
Always run generateProtoLibraries, to ensure we run deletion logic
shaldengeki Jul 5, 2025
634411c
Update WORKSPACE, too
shaldengeki Jul 5, 2025
30cecf4
Merge branch 'update-gazelle' into gazelle-plugin-generates-py-proto-…
shaldengeki Jul 5, 2025
1cabc18
Update shasum
shaldengeki Jul 5, 2025
9a38c26
Merge branch 'main' into update-gazelle
shaldengeki Jul 5, 2025
aa9e24a
Use golang 1.20.5
shaldengeki Jul 5, 2025
7fcc921
Merge branch 'update-gazelle' into gazelle-plugin-generates-py-proto-…
shaldengeki Jul 5, 2025
3d3c302
When python_proto_generate=false, ignore any pre-existing py_proto_li…
shaldengeki Jul 6, 2025
086c6aa
Merge branch 'main' into gazelle-plugin-generates-py-proto-library
shaldengeki Jul 6, 2025
cdeafde
Undo gazelle upgrade, and remove tests that depend on bzlmod support
shaldengeki Jul 6, 2025
18f34d1
Merge branch 'main' into gazelle-plugin-generates-py-proto-library
shaldengeki Jul 10, 2025
763ef9d
Wrap README.md to 80 chars per line, and be less-disruptive to existi…
shaldengeki Jul 12, 2025
b3805a9
Fix some language again
shaldengeki Jul 12, 2025
3e0fdcc
Use _py_proto suffix, to align with java, go naming schemes
shaldengeki Jul 12, 2025
0c9d436
Revert "Use _py_proto suffix, to align with java, go naming schemes"
shaldengeki Jul 13, 2025
eb87801
Revert "Undo gazelle upgrade, and remove tests that depend on bzlmod …
shaldengeki Jul 13, 2025
6791d4b
Undo version upgrade
shaldengeki Jul 13, 2025
66966fe
Fix a test's load ordering
shaldengeki Jul 13, 2025
d61aad1
Check in empty WORKSPACE files to make tests pass
shaldengeki Jul 13, 2025
b0039ee
_proto_py_pb2 -> _py_pb2
shaldengeki Jul 13, 2025
fa0d564
Merge branch 'main' into gazelle-plugin-generates-py-proto-library
dougthor42 Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ END_UNRELEASED_TEMPLATE
* 3.12.11
* 3.13.5
* 3.14.0b3
* (gazelle) New directive `gazelle:python_generate_proto`; when `true`,
Gazelle generates `py_proto_library` rules for `proto_library`. `false` by default.

{#v0-0-0-removed}
### Removed
Expand Down
4 changes: 2 additions & 2 deletions examples/bzlmod/py_proto_library/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ py_test(
srcs = ["test.py"],
main = "test.py",
deps = [
"//py_proto_library/example.com/proto:pricetag_proto_py_pb2",
"//py_proto_library/example.com/proto:pricetag_py_pb2",
],
)

py_test(
name = "message_test",
srcs = ["message_test.py"],
deps = [
"//py_proto_library/example.com/another_proto:message_proto_py_pb2",
"//py_proto_library/example.com/another_proto:message_py_pb2",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_python//python:proto.bzl", "py_proto_library")

py_proto_library(
name = "message_proto_py_pb2",
name = "message_py_pb2",
visibility = ["//visibility:public"],
deps = [":message_proto"],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_python//python:proto.bzl", "py_proto_library")

py_proto_library(
name = "pricetag_proto_py_pb2",
name = "pricetag_py_pb2",
visibility = ["//visibility:public"],
deps = [":pricetag_proto"],
)
Expand Down
4 changes: 2 additions & 2 deletions examples/py_proto_library/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ py_test(
srcs = ["test.py"],
main = "test.py",
deps = [
"//example.com/proto:pricetag_proto_py_pb2",
"//example.com/proto:pricetag_py_pb2",
],
)

py_test(
name = "message_test",
srcs = ["message_test.py"],
deps = [
"//example.com/another_proto:message_proto_py_pb2",
"//example.com/another_proto:message_py_pb2",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_python//python:proto.bzl", "py_proto_library")

py_proto_library(
name = "message_proto_py_pb2",
name = "message_py_pb2",
visibility = ["//visibility:public"],
deps = [":message_proto"],
)
Expand Down
2 changes: 1 addition & 1 deletion examples/py_proto_library/example.com/proto/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_python//python:proto.bzl", "py_proto_library")

py_proto_library(
name = "pricetag_proto_py_pb2",
name = "pricetag_py_pb2",
visibility = ["//visibility:public"],
deps = [":pricetag_proto"],
)
Expand Down
37 changes: 37 additions & 0 deletions gazelle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ Python-specific directives are as follows:
| Controls whether Gazelle resolves dependencies for import statements that use paths relative to the current package. Can be "true" or "false".|
| `# gazelle:python_generate_pyi_deps` | `false` |
| Controls whether to generate a separate `pyi_deps` attribute for type-checking dependencies or merge them into the regular `deps` attribute. When `false` (default), type-checking dependencies are merged into `deps` for backward compatibility. When `true`, generates separate `pyi_deps`. Imports in blocks with the format `if typing.TYPE_CHECKING:`/`if TYPE_CHECKING:` and type-only stub packages (eg. boto3-stubs) are recognized as type-checking dependencies. |
| [`# gazelle:python_generate_proto`](#directive-python_generate_proto) | `false` |
| Controls whether to generate a `py_proto_library` for each `proto_library` in the package. By default we load this rule from the `@protobuf` repository; use `gazelle:map_kind` if you need to load this from somewhere else. |

#### Directive: `python_root`:

Expand Down Expand Up @@ -484,6 +486,41 @@ def py_test(name, main=None, **kwargs):
)
```

#### Directive: `python_generate_proto`:

When `# gazelle:python_generate_proto true`, Gazelle will generate one
`py_proto_library` for each `proto_library`, generating Python clients for
protobuf in each package. By default this is turned off. Gazelle will also
generate a load statement for the `py_proto_library` - attempting to detect
the configured name for the `@protobuf` / `@com_google_protobuf` repo in your
`MODULE.bazel`, and otherwise falling back to `@com_google_protobuf` for
compatibility with `WORKSPACE`.

For example, in a package with `# gazelle:python_generate_proto true` and a
`foo.proto`, if you have both the proto extension and the Python extension
loaded into Gazelle, you'll get something like:

```starlark
load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")

# gazelle:python_generate_proto true

proto_library(
name = "foo_proto",
srcs = ["foo.proto"],
visibility = ["//:__subpackages__"],
)

py_proto_library(
name = "foo_py_pb2",
visibility = ["//:__subpackages__"],
deps = [":foo_proto"],
)
```

When `false`, Gazelle will ignore any `py_proto_library`, including previously-generated or hand-created rules.

### Annotations

*Annotations* refer to comments found _within Python files_ that configure how
Expand Down
6 changes: 5 additions & 1 deletion gazelle/python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ go_library(
"@bazel_gazelle//config:go_default_library",
"@bazel_gazelle//label:go_default_library",
"@bazel_gazelle//language:go_default_library",
"@bazel_gazelle//language/proto:go_default_library",
"@bazel_gazelle//repo:go_default_library",
"@bazel_gazelle//resolve:go_default_library",
"@bazel_gazelle//rule:go_default_library",
Expand Down Expand Up @@ -91,7 +92,10 @@ gazelle_test(

gazelle_binary(
name = "gazelle_binary",
languages = [":python"],
languages = [
"@bazel_gazelle//language/proto",
":python",
],
visibility = ["//visibility:public"],
)

Expand Down
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.LabelNormalization,
pythonconfig.GeneratePyiDeps,
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
}
}

Expand Down Expand Up @@ -237,6 +238,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetGeneratePyiDeps(v)
case pythonconfig.GenerateProto:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetGenerateProto(v)
}
}

Expand Down
52 changes: 52 additions & 0 deletions gazelle/python/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
var result language.GenerateResult
result.Gen = make([]*rule.Rule, 0)

if cfg.GenerateProto() {
generateProtoLibraries(args, pythonProjectRoot, visibility, &result)
}

collisionErrors := singlylinkedlist.New()

appendPyLibrary := func(srcs *treeset.Set, pyLibraryTargetName string) {
Expand Down Expand Up @@ -551,3 +555,51 @@ func ensureNoCollision(file *rule.File, targetName, kind string) error {
}
return nil
}

func generateProtoLibraries(args language.GenerateArgs, pythonProjectRoot string, visibility []string, res *language.GenerateResult) {
// First, enumerate all the proto_library in this package.
var protoRuleNames []string
for _, r := range args.OtherGen {
if r.Kind() != "proto_library" {
continue
}
protoRuleNames = append(protoRuleNames, r.Name())
}
sort.Strings(protoRuleNames)

// Next, enumerate all the pre-existing py_proto_library in this package, so we can delete unnecessary rules later.
pyProtoRules := map[string]bool{}
if args.File != nil {
for _, r := range args.File.Rules {
if r.Kind() == "py_proto_library" {
pyProtoRules[r.Name()] = false
}
}
}

emptySiblings := treeset.Set{}
// Generate a py_proto_library for each proto_library.
for _, protoRuleName := range protoRuleNames {
pyProtoLibraryName := strings.TrimSuffix(protoRuleName, "_proto") + "_py_pb2"
pyProtoLibrary := newTargetBuilder(pyProtoLibraryKind, pyProtoLibraryName, pythonProjectRoot, args.Rel, &emptySiblings).
addVisibility(visibility).
addResolvedDependency(":" + protoRuleName).
generateImportsAttribute().build()

res.Gen = append(res.Gen, pyProtoLibrary)
res.Imports = append(res.Imports, pyProtoLibrary.PrivateAttr(config.GazelleImportsKey))
pyProtoRules[pyProtoLibrary.Name()] = true

}

// Finally, emit an empty rule for each pre-existing py_proto_library that we didn't already generate.
for ruleName, generated := range pyProtoRules {
if generated {
continue
}

emptyRule := newTargetBuilder(pyProtoLibraryKind, ruleName, pythonProjectRoot, args.Rel, &emptySiblings).build()
res.Empty = append(res.Empty, emptyRule)
}

}
60 changes: 43 additions & 17 deletions gazelle/python/kinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
package python

import (
"fmt"

"github.com/bazelbuild/bazel-gazelle/rule"
)

const (
pyBinaryKind = "py_binary"
pyLibraryKind = "py_library"
pyTestKind = "py_test"
pyBinaryKind = "py_binary"
pyLibraryKind = "py_library"
pyProtoLibraryKind = "py_proto_library"
pyTestKind = "py_test"
)

// Kinds returns a map that maps rule names (kinds) and information on how to
Expand All @@ -32,7 +35,7 @@ func (*Python) Kinds() map[string]rule.KindInfo {

var pyKinds = map[string]rule.KindInfo{
pyBinaryKind: {
MatchAny: false,
MatchAny: false,
MatchAttrs: []string{"srcs"},
NonEmptyAttrs: map[string]bool{
"deps": true,
Expand All @@ -45,7 +48,7 @@ var pyKinds = map[string]rule.KindInfo{
"srcs": true,
},
ResolveAttrs: map[string]bool{
"deps": true,
"deps": true,
"pyi_deps": true,
},
},
Expand All @@ -62,10 +65,16 @@ var pyKinds = map[string]rule.KindInfo{
"srcs": true,
},
ResolveAttrs: map[string]bool{
"deps": true,
"deps": true,
"pyi_deps": true,
},
},
pyProtoLibraryKind: {
NonEmptyAttrs: map[string]bool{
"deps": true,
},
ResolveAttrs: map[string]bool{"deps": true},
},
pyTestKind: {
MatchAny: false,
NonEmptyAttrs: map[string]bool{
Expand All @@ -79,26 +88,43 @@ var pyKinds = map[string]rule.KindInfo{
"srcs": true,
},
ResolveAttrs: map[string]bool{
"deps": true,
"deps": true,
"pyi_deps": true,
},
},
}

func (py *Python) Loads() []rule.LoadInfo {
panic("ApparentLoads should be called instead")
}

// Loads returns .bzl files and symbols they define. Every rule generated by
// GenerateRules, now or in the past, should be loadable from one of these
// files.
func (py *Python) Loads() []rule.LoadInfo {
return pyLoads
func (py *Python) ApparentLoads(moduleToApparentName func(string) string) []rule.LoadInfo {
return apparentLoads(moduleToApparentName)
}

var pyLoads = []rule.LoadInfo{
{
Name: "@rules_python//python:defs.bzl",
Symbols: []string{
pyBinaryKind,
pyLibraryKind,
pyTestKind,
func apparentLoads(moduleToApparentName func(string) string) []rule.LoadInfo {
protobuf := moduleToApparentName("protobuf")
if protobuf == "" {
protobuf = "com_google_protobuf"
}

return []rule.LoadInfo{
{
Name: "@rules_python//python:defs.bzl",
Symbols: []string{
pyBinaryKind,
pyLibraryKind,
pyTestKind,
},
},
},
{
Name: fmt.Sprintf("@%s//bazel:py_proto_library.bzl", protobuf),
Symbols: []string{
pyProtoLibraryKind,
},
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Directive: `python_generate_proto`

This test case asserts that the `# gazelle:python_generate_proto` directive
correctly:

1. Uses the default value when `python_generate_proto` is not set.
2. Generates (or not) `py_proto_library` when `python_generate_proto` is set, based on whether a proto is present.

[gh-2994]: https://github.com/bazel-contrib/rules_python/issues/2994
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This is a Bazel workspace for the Gazelle test data.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@rules_proto//proto:defs.bzl", "proto_library")

# python_generate_proto is not set, so py_proto_library is not generated.

proto_library(
name = "foo_proto",
srcs = ["foo.proto"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@rules_proto//proto:defs.bzl", "proto_library")

# python_generate_proto is not set, so py_proto_library is not generated.

proto_library(
name = "foo_proto",
srcs = ["foo.proto"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto3";

package foo;

message Foo {
string bar = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# python_generate_proto is not set, so py_proto_library is not generated.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# python_generate_proto is not set, so py_proto_library is not generated.
Loading