123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- # Copyright 2021 The gRPC Authors
- #
- # 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.
- """Generates and compiles Python gRPC stubs from proto_library rules."""
- load("@rules_proto//proto:defs.bzl", "ProtoInfo")
- load(
- "//bazel:protobuf.bzl",
- "declare_out_files",
- "get_include_directory",
- "get_out_dir",
- "get_plugin_args",
- "get_proto_arguments",
- "get_staged_proto_file",
- "includes_from_deps",
- "is_well_known",
- "protos_from_context",
- )
- _GENERATED_PROTO_FORMAT = "{}_pb2.py"
- _GENERATED_GRPC_PROTO_FORMAT = "{}_pb2_grpc.py"
- PyProtoInfo = provider(
- "The Python outputs from the Protobuf compiler.",
- fields = {
- "py_info": "A PyInfo provider for the generated code.",
- "generated_py_srcs": "The direct (not transitive) generated Python source files.",
- },
- )
- def _merge_pyinfos(pyinfos):
- return PyInfo(
- transitive_sources = depset(transitive = [p.transitive_sources for p in pyinfos]),
- imports = depset(transitive = [p.imports for p in pyinfos]),
- )
- def _gen_py_aspect_impl(target, context):
- # Early return for well-known protos.
- if is_well_known(str(context.label)):
- return [
- PyProtoInfo(py_info = context.attr._protobuf_library[PyInfo]),
- ]
- protos = []
- for p in target[ProtoInfo].direct_sources:
- protos.append(get_staged_proto_file(target.label, context, p))
- includes = depset(direct = protos, transitive = [target[ProtoInfo].transitive_imports])
- out_files = declare_out_files(protos, context, _GENERATED_PROTO_FORMAT)
- generated_py_srcs = out_files
- tools = [context.executable._protoc]
- out_dir = get_out_dir(protos, context)
- arguments = ([
- "--python_out={}".format(out_dir.path),
- ] + [
- "--proto_path={}".format(get_include_directory(i))
- for i in includes.to_list()
- ] + [
- "--proto_path={}".format(context.genfiles_dir.path),
- ])
- arguments += get_proto_arguments(protos, context.genfiles_dir.path)
- context.actions.run(
- inputs = protos + includes.to_list(),
- tools = tools,
- outputs = out_files,
- executable = context.executable._protoc,
- arguments = arguments,
- mnemonic = "ProtocInvocation",
- )
- imports = []
- if out_dir.import_path:
- imports.append("{}/{}".format(context.workspace_name, out_dir.import_path))
- py_info = PyInfo(transitive_sources = depset(direct = out_files), imports = depset(direct = imports))
- return PyProtoInfo(
- py_info = _merge_pyinfos(
- [
- py_info,
- context.attr._protobuf_library[PyInfo],
- ] + [dep[PyProtoInfo].py_info for dep in context.rule.attr.deps],
- ),
- generated_py_srcs = generated_py_srcs,
- )
- _gen_py_aspect = aspect(
- implementation = _gen_py_aspect_impl,
- attr_aspects = ["deps"],
- fragments = ["py"],
- attrs = {
- "_protoc": attr.label(
- default = Label("//external:protocol_compiler"),
- providers = ["files_to_run"],
- executable = True,
- cfg = "host",
- ),
- "_protobuf_library": attr.label(
- default = Label("@com_google_protobuf//:protobuf_python"),
- providers = [PyInfo],
- ),
- },
- )
- def _generate_py_impl(context):
- if (len(context.attr.deps) != 1):
- fail("Can only compile a single proto at a time.")
- py_sources = []
- # If the proto_library this rule *directly* depends on is in another
- # package, then we generate .py files to import them in this package. This
- # behavior is needed to allow rearranging of import paths to make Bazel
- # outputs align with native python workflows.
- #
- # Note that this approach is vulnerable to protoc defining __all__ or other
- # symbols with __ prefixes that need to be directly imported. Since these
- # names are likely to be reserved for private APIs, the risk is minimal.
- if context.label.package != context.attr.deps[0].label.package:
- for py_src in context.attr.deps[0][PyProtoInfo].generated_py_srcs:
- reimport_py_file = context.actions.declare_file(py_src.basename)
- py_sources.append(reimport_py_file)
- import_line = "from %s import *" % py_src.short_path.replace("/", ".")[:-len(".py")]
- context.actions.write(reimport_py_file, import_line)
- # Collect output PyInfo provider.
- imports = [context.label.package + "/" + i for i in context.attr.imports]
- py_info = PyInfo(transitive_sources = depset(direct = py_sources), imports = depset(direct = imports))
- out_pyinfo = _merge_pyinfos([py_info, context.attr.deps[0][PyProtoInfo].py_info])
- runfiles = context.runfiles(files = out_pyinfo.transitive_sources.to_list()).merge(context.attr._protobuf_library[DefaultInfo].data_runfiles)
- return [
- DefaultInfo(
- files = out_pyinfo.transitive_sources,
- runfiles = runfiles,
- ),
- out_pyinfo,
- ]
- py_proto_library = rule(
- attrs = {
- "deps": attr.label_list(
- mandatory = True,
- allow_empty = False,
- providers = [ProtoInfo],
- aspects = [_gen_py_aspect],
- ),
- "_protoc": attr.label(
- default = Label("//external:protocol_compiler"),
- providers = ["files_to_run"],
- executable = True,
- cfg = "host",
- ),
- "_protobuf_library": attr.label(
- default = Label("@com_google_protobuf//:protobuf_python"),
- providers = [PyInfo],
- ),
- "imports": attr.string_list(),
- },
- implementation = _generate_py_impl,
- )
- def _generate_pb2_grpc_src_impl(context):
- protos = protos_from_context(context)
- includes = includes_from_deps(context.attr.deps)
- out_files = declare_out_files(protos, context, _GENERATED_GRPC_PROTO_FORMAT)
- plugin_flags = ["grpc_2_0"] + context.attr.strip_prefixes
- arguments = []
- tools = [context.executable._protoc, context.executable._grpc_plugin]
- out_dir = get_out_dir(protos, context)
- arguments += get_plugin_args(
- context.executable._grpc_plugin,
- plugin_flags,
- out_dir.path,
- False,
- )
- arguments += [
- "--proto_path={}".format(get_include_directory(i))
- for i in includes
- ]
- arguments.append("--proto_path={}".format(context.genfiles_dir.path))
- arguments += get_proto_arguments(protos, context.genfiles_dir.path)
- context.actions.run(
- inputs = protos + includes,
- tools = tools,
- outputs = out_files,
- executable = context.executable._protoc,
- arguments = arguments,
- mnemonic = "ProtocInvocation",
- )
- p = PyInfo(transitive_sources = depset(direct = out_files))
- py_info = _merge_pyinfos(
- [
- p,
- context.attr._grpc_library[PyInfo],
- ] + [dep[PyInfo] for dep in context.attr.py_deps],
- )
- runfiles = context.runfiles(files = out_files, transitive_files = py_info.transitive_sources).merge(context.attr._grpc_library[DefaultInfo].data_runfiles)
- return [
- DefaultInfo(
- files = depset(direct = out_files),
- runfiles = runfiles,
- ),
- py_info,
- ]
- _generate_pb2_grpc_src = rule(
- attrs = {
- "deps": attr.label_list(
- mandatory = True,
- allow_empty = False,
- providers = [ProtoInfo],
- ),
- "py_deps": attr.label_list(
- mandatory = True,
- allow_empty = False,
- providers = [PyInfo],
- ),
- "strip_prefixes": attr.string_list(),
- "_grpc_plugin": attr.label(
- executable = True,
- providers = ["files_to_run"],
- cfg = "host",
- default = Label("//src/compiler:grpc_python_plugin"),
- ),
- "_protoc": attr.label(
- executable = True,
- providers = ["files_to_run"],
- cfg = "host",
- default = Label("//external:protocol_compiler"),
- ),
- "_grpc_library": attr.label(
- default = Label("//src/python/grpcio/grpc:grpcio"),
- providers = [PyInfo],
- ),
- },
- implementation = _generate_pb2_grpc_src_impl,
- )
- def py_grpc_library(
- name,
- srcs,
- deps,
- strip_prefixes = [],
- **kwargs):
- """Generate python code for gRPC services defined in a protobuf.
- Args:
- name: The name of the target.
- srcs: (List of `labels`) a single proto_library target containing the
- schema of the service.
- deps: (List of `labels`) a single py_proto_library target for the
- proto_library in `srcs`.
- strip_prefixes: (List of `strings`) If provided, this prefix will be
- stripped from the beginning of foo_pb2 modules imported by the
- generated stubs. This is useful in combination with the `imports`
- attribute of the `py_library` rule.
- **kwargs: Additional arguments to be supplied to the invocation of
- py_library.
- """
- if len(srcs) != 1:
- fail("Can only compile a single proto at a time.")
- if len(deps) != 1:
- fail("Deps must have length 1.")
- _generate_pb2_grpc_src(
- name = name,
- deps = srcs,
- py_deps = deps,
- strip_prefixes = strip_prefixes,
- **kwargs
- )
|