• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The gRPC Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Generates and compiles Python gRPC stubs from proto_library rules."""
15
16load("@rules_proto//proto:defs.bzl", "ProtoInfo")
17load("@rules_python//python:py_info.bzl", "PyInfo")
18load(
19    "//bazel:protobuf.bzl",
20    "declare_out_files",
21    "get_include_directory",
22    "get_out_dir",
23    "get_plugin_args",
24    "get_proto_arguments",
25    "get_staged_proto_file",
26    "includes_from_deps",
27    "is_well_known",
28    "protos_from_context",
29)
30
31_GENERATED_PROTO_FORMAT = "{}_pb2.py"
32_GENERATED_PROTO_STUB_FORMAT = "{}_pb2.pyi"
33_GENERATED_GRPC_PROTO_FORMAT = "{}_pb2_grpc.py"
34
35PyProtoInfo = provider(
36    "The Python outputs from the Protobuf compiler.",
37    fields = {
38        "py_info": "A PyInfo provider for the generated code.",
39        "generated_py_srcs": "The direct (not transitive) generated Python source files.",
40    },
41)
42
43def _merge_pyinfos(pyinfos):
44    return PyInfo(
45        transitive_sources = depset(transitive = [p.transitive_sources for p in pyinfos]),
46        imports = depset(transitive = [p.imports for p in pyinfos]),
47    )
48
49def _gen_py_aspect_impl(target, context):
50    # Early return for well-known protos.
51    if is_well_known(str(context.label)):
52        return [
53            PyProtoInfo(
54                py_info = context.attr._protobuf_library[PyInfo],
55                generated_py_srcs = [],
56            ),
57        ]
58
59    protos = []
60    for p in target[ProtoInfo].direct_sources:
61        protos.append(get_staged_proto_file(target.label, context, p))
62
63    includes = depset(direct = protos, transitive = [target[ProtoInfo].transitive_imports])
64    out_files = (declare_out_files(protos, context, _GENERATED_PROTO_FORMAT) +
65                 declare_out_files(protos, context, _GENERATED_PROTO_STUB_FORMAT))
66    generated_py_srcs = out_files
67
68    tools = [context.executable._protoc]
69
70    out_dir = get_out_dir(protos, context)
71
72    arguments = ([
73        "--python_out={}".format(out_dir.path),
74        "--pyi_out={}".format(out_dir.path),
75    ] + [
76        "--proto_path={}".format(get_include_directory(i))
77        for i in includes.to_list()
78    ] + [
79        "--proto_path={}".format(context.genfiles_dir.path),
80    ])
81
82    arguments += get_proto_arguments(protos, context.genfiles_dir.path)
83
84    context.actions.run(
85        inputs = protos + includes.to_list(),
86        tools = tools,
87        outputs = out_files,
88        executable = context.executable._protoc,
89        arguments = arguments,
90        mnemonic = "ProtocInvocation",
91    )
92
93    imports = []
94    if out_dir.import_path:
95        imports.append("{}/{}".format(context.workspace_name, out_dir.import_path))
96
97    py_info = PyInfo(transitive_sources = depset(direct = out_files), imports = depset(direct = imports))
98    return PyProtoInfo(
99        py_info = _merge_pyinfos(
100            [
101                py_info,
102                context.attr._protobuf_library[PyInfo],
103            ] + [dep[PyProtoInfo].py_info for dep in context.rule.attr.deps],
104        ),
105        generated_py_srcs = generated_py_srcs,
106    )
107
108_gen_py_aspect = aspect(
109    implementation = _gen_py_aspect_impl,
110    attr_aspects = ["deps"],
111    fragments = ["py"],
112    attrs = {
113        "_protoc": attr.label(
114            default = Label("@com_google_protobuf//:protoc"),
115            providers = ["files_to_run"],
116            executable = True,
117            cfg = "exec",
118        ),
119        "_protobuf_library": attr.label(
120            default = Label("@com_google_protobuf//:protobuf_python"),
121            providers = [PyInfo],
122        ),
123    },
124)
125
126def _generate_py_impl(context):
127    if (len(context.attr.deps) != 1):
128        fail("Can only compile a single proto at a time.")
129
130    py_sources = []
131
132    # If the proto_library this rule *directly* depends on is in another
133    # package, then we generate .py files to import them in this package. This
134    # behavior is needed to allow rearranging of import paths to make Bazel
135    # outputs align with native python workflows.
136    #
137    # Note that this approach is vulnerable to protoc defining __all__ or other
138    # symbols with __ prefixes that need to be directly imported. Since these
139    # names are likely to be reserved for private APIs, the risk is minimal.
140    if context.label.package != context.attr.deps[0].label.package:
141        for py_src in context.attr.deps[0][PyProtoInfo].generated_py_srcs:
142            reimport_py_file = context.actions.declare_file(py_src.basename)
143            py_sources.append(reimport_py_file)
144            import_line = "from %s import *" % py_src.short_path.replace("..", "external").replace("/", ".")[:-len(".py")]
145            context.actions.write(reimport_py_file, import_line)
146
147    # Collect output PyInfo provider.
148    imports = [context.label.package + "/" + i for i in context.attr.imports]
149    py_info = PyInfo(transitive_sources = depset(direct = py_sources), imports = depset(direct = imports))
150    out_pyinfo = _merge_pyinfos([py_info, context.attr.deps[0][PyProtoInfo].py_info])
151
152    runfiles = context.runfiles(files = out_pyinfo.transitive_sources.to_list()).merge(context.attr._protobuf_library[DefaultInfo].data_runfiles)
153    return [
154        DefaultInfo(
155            files = out_pyinfo.transitive_sources,
156            runfiles = runfiles,
157        ),
158        out_pyinfo,
159    ]
160
161py_proto_library = rule(
162    attrs = {
163        "deps": attr.label_list(
164            mandatory = True,
165            allow_empty = False,
166            providers = [ProtoInfo],
167            aspects = [_gen_py_aspect],
168        ),
169        "_protoc": attr.label(
170            default = Label("@com_google_protobuf//:protoc"),
171            providers = ["files_to_run"],
172            executable = True,
173            cfg = "exec",
174        ),
175        "_protobuf_library": attr.label(
176            default = Label("@com_google_protobuf//:protobuf_python"),
177            providers = [PyInfo],
178        ),
179        "imports": attr.string_list(),
180    },
181    implementation = _generate_py_impl,
182)
183
184def _generate_pb2_grpc_src_impl(context):
185    protos = protos_from_context(context)
186    includes = includes_from_deps(context.attr.deps)
187    out_files = declare_out_files(protos, context, _GENERATED_GRPC_PROTO_FORMAT)
188
189    plugin_flags = ["grpc_2_0"] + context.attr.strip_prefixes
190
191    arguments = []
192    tools = [context.executable._protoc, context.executable._grpc_plugin]
193    out_dir = get_out_dir(protos, context)
194    if out_dir.import_path:
195        # is virtual imports
196        out_path = out_dir.path
197    else:
198        out_path = context.genfiles_dir.path
199    arguments += get_plugin_args(
200        context.executable._grpc_plugin,
201        plugin_flags,
202        out_path,
203        False,
204    )
205
206    arguments += [
207        "--proto_path={}".format(get_include_directory(i))
208        for i in includes
209    ]
210    arguments.append("--proto_path={}".format(context.genfiles_dir.path))
211    arguments += get_proto_arguments(protos, context.genfiles_dir.path)
212
213    context.actions.run(
214        inputs = protos + includes,
215        tools = tools,
216        outputs = out_files,
217        executable = context.executable._protoc,
218        arguments = arguments,
219        mnemonic = "ProtocInvocation",
220    )
221
222    p = PyInfo(transitive_sources = depset(direct = out_files))
223    py_info = _merge_pyinfos(
224        [
225            p,
226            context.attr.grpc_library[PyInfo],
227        ] + [dep[PyInfo] for dep in context.attr.py_deps],
228    )
229
230    runfiles = context.runfiles(files = out_files, transitive_files = py_info.transitive_sources).merge(context.attr.grpc_library[DefaultInfo].data_runfiles)
231
232    return [
233        DefaultInfo(
234            files = depset(direct = out_files),
235            runfiles = runfiles,
236        ),
237        py_info,
238    ]
239
240_generate_pb2_grpc_src = rule(
241    attrs = {
242        "deps": attr.label_list(
243            mandatory = True,
244            allow_empty = False,
245            providers = [ProtoInfo],
246        ),
247        "py_deps": attr.label_list(
248            mandatory = True,
249            allow_empty = False,
250            providers = [PyInfo],
251        ),
252        "strip_prefixes": attr.string_list(),
253        "_grpc_plugin": attr.label(
254            executable = True,
255            providers = ["files_to_run"],
256            cfg = "exec",
257            default = Label("//src/compiler:grpc_python_plugin"),
258        ),
259        "_protoc": attr.label(
260            executable = True,
261            providers = ["files_to_run"],
262            cfg = "exec",
263            default = Label("@com_google_protobuf//:protoc"),
264        ),
265        "grpc_library": attr.label(
266            default = Label("//src/python/grpcio/grpc:grpcio"),
267            providers = [PyInfo],
268        ),
269    },
270    implementation = _generate_pb2_grpc_src_impl,
271)
272
273def py_grpc_library(
274        name,
275        srcs,
276        deps,
277        strip_prefixes = [],
278        grpc_library = Label("//src/python/grpcio/grpc:grpcio"),
279        **kwargs):
280    """Generate python code for gRPC services defined in a protobuf.
281
282    Args:
283      name: The name of the target.
284      srcs: (List of `labels`) a single proto_library target containing the
285        schema of the service.
286      deps: (List of `labels`) a single py_proto_library target for the
287        proto_library in `srcs`.
288      strip_prefixes: (List of `strings`) If provided, this prefix will be
289        stripped from the beginning of foo_pb2 modules imported by the
290        generated stubs. This is useful in combination with the `imports`
291        attribute of the `py_library` rule.
292      grpc_library: (`label`) a single `py_library` target representing the
293        python gRPC library target to be depended upon. This can be used to
294        generate code that depends on `grpcio` from the Python Package Index.
295      **kwargs: Additional arguments to be supplied to the invocation of
296        py_library.
297    """
298    if len(srcs) != 1:
299        fail("Can only compile a single proto at a time.")
300
301    if len(deps) != 1:
302        fail("Deps must have length 1.")
303
304    _generate_pb2_grpc_src(
305        name = name,
306        deps = srcs,
307        py_deps = deps,
308        strip_prefixes = strip_prefixes,
309        grpc_library = grpc_library,
310        **kwargs
311    )
312