Implement py_proto_library (#832)
* Add py_proto_library
* Bump versions of rules_proto and protobuf
* Update documentation
* Bump rules_pkg version
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index c99b040..75e48bc 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -19,7 +19,7 @@
filegroup(
name = "distribution",
- srcs = glob(["**"]),
+ srcs = glob(["**"]) + ["//python/private/proto:distribution"],
visibility = ["//python:__pkg__"],
)
diff --git a/python/private/proto/BUILD b/python/private/proto/BUILD
new file mode 100644
index 0000000..8483d19
--- /dev/null
+++ b/python/private/proto/BUILD
@@ -0,0 +1,32 @@
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@rules_proto//proto:defs.bzl", "proto_lang_toolchain")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"]) # Apache 2.0
+
+filegroup(
+ name = "distribution",
+ srcs = glob(["**"]),
+ visibility = ["//python/private:__pkg__"],
+)
+
+proto_lang_toolchain(
+ name = "python_toolchain",
+ command_line = "--python_out=%s",
+ progress_message = "Generating Python proto_library %{label}",
+ runtime = "@com_google_protobuf//:protobuf_python",
+)
diff --git a/python/private/proto/py_proto_library.bzl b/python/private/proto/py_proto_library.bzl
new file mode 100644
index 0000000..ef5f2ca
--- /dev/null
+++ b/python/private/proto/py_proto_library.bzl
@@ -0,0 +1,191 @@
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""The implementation of the `py_proto_library` rule and its aspect."""
+
+load("@rules_proto//proto:defs.bzl", "ProtoInfo", "proto_common")
+load("//python:defs.bzl", "PyInfo")
+
+ProtoLangToolchainInfo = proto_common.ProtoLangToolchainInfo
+
+_PyProtoInfo = provider(
+ doc = "Encapsulates information needed by the Python proto rules.",
+ fields = {
+ "runfiles_from_proto_deps": """
+ (depset[File]) Files from the transitive closure implicit proto
+ dependencies""",
+ "transitive_sources": """(depset[File]) The Python sources.""",
+ },
+)
+
+def _filter_provider(provider, *attrs):
+ return [dep[provider] for attr in attrs for dep in attr if provider in dep]
+
+def _py_proto_aspect_impl(target, ctx):
+ """Generates and compiles Python code for a proto_library.
+
+ The function runs protobuf compiler on the `proto_library` target generating
+ a .py file for each .proto file.
+
+ Args:
+ target: (Target) A target providing `ProtoInfo`. Usually this means a
+ `proto_library` target, but not always; you must expect to visit
+ non-`proto_library` targets, too.
+ ctx: (RuleContext) The rule context.
+
+ Returns:
+ ([_PyProtoInfo]) Providers collecting transitive information about
+ generated files.
+ """
+
+ _proto_library = ctx.rule.attr
+
+ # Check Proto file names
+ for proto in target[ProtoInfo].direct_sources:
+ if proto.is_source and "-" in proto.dirname:
+ fail("Cannot generate Python code for a .proto whose path contains '-' ({}).".format(
+ proto.path,
+ ))
+
+ proto_lang_toolchain_info = ctx.attr._aspect_proto_toolchain[ProtoLangToolchainInfo]
+ api_deps = [proto_lang_toolchain_info.runtime]
+
+ generated_sources = []
+ proto_info = target[ProtoInfo]
+ if proto_info.direct_sources:
+ # Generate py files
+ generated_sources = proto_common.declare_generated_files(
+ actions = ctx.actions,
+ proto_info = proto_info,
+ extension = "_pb2.py",
+ name_mapper = lambda name: name.replace("-", "_").replace(".", "/"),
+ )
+
+ proto_common.compile(
+ actions = ctx.actions,
+ proto_info = proto_info,
+ proto_lang_toolchain_info = proto_lang_toolchain_info,
+ generated_files = generated_sources,
+ plugin_output = ctx.bin_dir.path,
+ )
+
+ # Generated sources == Python sources
+ python_sources = generated_sources
+
+ deps = _filter_provider(_PyProtoInfo, getattr(_proto_library, "deps", []))
+ runfiles_from_proto_deps = depset(
+ transitive = [dep[DefaultInfo].default_runfiles.files for dep in api_deps] +
+ [dep.runfiles_from_proto_deps for dep in deps],
+ )
+ transitive_sources = depset(
+ direct = python_sources,
+ transitive = [dep.transitive_sources for dep in deps],
+ )
+
+ return [
+ _PyProtoInfo(
+ runfiles_from_proto_deps = runfiles_from_proto_deps,
+ transitive_sources = transitive_sources,
+ ),
+ ]
+
+_py_proto_aspect = aspect(
+ implementation = _py_proto_aspect_impl,
+ attrs = {
+ "_aspect_proto_toolchain": attr.label(
+ default = ":python_toolchain",
+ ),
+ },
+ attr_aspects = ["deps"],
+ required_providers = [ProtoInfo],
+ provides = [_PyProtoInfo],
+)
+
+def _py_proto_library_rule(ctx):
+ """Merges results of `py_proto_aspect` in `deps`.
+
+ Args:
+ ctx: (RuleContext) The rule context.
+ Returns:
+ ([PyInfo, DefaultInfo, OutputGroupInfo])
+ """
+ if not ctx.attr.deps:
+ fail("'deps' attribute mustn't be empty.")
+
+ pyproto_infos = _filter_provider(_PyProtoInfo, ctx.attr.deps)
+ default_outputs = depset(
+ transitive = [info.transitive_sources for info in pyproto_infos],
+ )
+
+ return [
+ DefaultInfo(
+ files = default_outputs,
+ default_runfiles = ctx.runfiles(transitive_files = depset(
+ transitive =
+ [default_outputs] +
+ [info.runfiles_from_proto_deps for info in pyproto_infos],
+ )),
+ ),
+ OutputGroupInfo(
+ default = depset(),
+ ),
+ PyInfo(
+ transitive_sources = default_outputs,
+ # Proto always produces 2- and 3- compatible source files
+ has_py2_only_sources = False,
+ has_py3_only_sources = False,
+ ),
+ ]
+
+py_proto_library = rule(
+ implementation = _py_proto_library_rule,
+ doc = """
+ Use `py_proto_library` to generate Python libraries from `.proto` files.
+
+ The convention is to name the `py_proto_library` rule `foo_py_pb2`,
+ when it is wrapping `proto_library` rule `foo_proto`.
+
+ `deps` must point to a `proto_library` rule.
+
+ Example:
+
+```starlark
+py_library(
+ name = "lib",
+ deps = [":foo_py_pb2"],
+)
+
+py_proto_library(
+ name = "foo_py_pb2",
+ deps = [":foo_proto"],
+)
+
+proto_library(
+ name = "foo_proto",
+ srcs = ["foo.proto"],
+)
+```""",
+ attrs = {
+ "deps": attr.label_list(
+ doc = """
+ The list of `proto_library` rules to generate Python libraries for.
+
+ Usually this is just the one target: the proto library of interest.
+ It can be any target providing `ProtoInfo`.""",
+ providers = [ProtoInfo],
+ aspects = [_py_proto_aspect],
+ ),
+ },
+ provides = [PyInfo],
+)
diff --git a/python/proto.bzl b/python/proto.bzl
new file mode 100644
index 0000000..3f455ae
--- /dev/null
+++ b/python/proto.bzl
@@ -0,0 +1,21 @@
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Python proto library.
+"""
+
+load("//python/private/proto:py_proto_library.bzl", _py_proto_library = "py_proto_library")
+
+py_proto_library = _py_proto_library