I recently ran in to a minor issue using gRPC in Python. It turns out
that there are constraints on the .proto directory structure if you
want to build a Python package out of multiple .proto files. This
constraint doesn't appear to affect the Maven or NuGet plugins, which
caused me to waste a bunch of time since I got those working first and
assumed that other language plugins would be the same. The issue and solution are
described in detail in this
GitHub issue.
In the following, I'll go over an example that reproduces my initial
mistake and a fixed version.
The gRPC developers in the GitHub issue recommend keeping
all the protobuf definitions for a package in a single file. In that
case, you won't have this problem at all. However, the workaround
presented here is not onerous, especially if followed from the start.
For concreteness, there is a
simple demo that
illustrates both the incorrect and working structures.
The initial setup I had was the following directory structure
- proto/
- message.proto
- service.proto
- demo/
With message.proto
syntax = "proto3";
package twwhatever.demo;
message DemoMessage {
string m = 1;
}
and service.proto
syntax = "proto3";
package twwhatever.demo;
import "message.proto";
service DemoService {
rpc Function (DemoMessage) returns (DemoMessage) {}
}
My plan was to generate the Python bindings using the following
command in the demo directory via
python -m grpc_tools.protoc \
-I../proto-wrong/ \
--python_out=twwhatever/demo \
--grpc_python_out=twwhatever/demo \
../proto-wrong/*.proto
That command succeeded and generates the bindings as expected. When I
tried to create a service, though, I got an error.
from twwhatever.demo import service_pb2_grpc
from twwhatever.demo import message_pb2
class Demo(service_pb2_grpc.DemoServiceServicer):
def Function(self, request, context):
return message_pb2.DemoMessage(m='Hi there!')
if __name__ == '__main__':
demo = Demo()
print(demo.Function(None, None))
Unfortunately, importing the definitions in service.proto doesn't
work because the generated code service_pb2_grpc.proto contains the statement
import message_pb2 as message__pb2
which doesn't work because message_pb2 is actually in the twwhatever.demo module.
There appear to be a couple workarounds:
* Keep all the protocol buffer definitions you need in a single file
* Mirror the directory structure of your protocol buffer definitions - in particular your import statements - with your target Python package
In this case, it meant setting up the repository like this
- proto/twwhatever/demo/
- message.proto
- service.proto
- demo/
Changing the import statement in service.proto to
import "twwhatever/demo/message.proto"
And invoking the command from the Python directory like
python -m grpc_tools.protoc \
-I../proto/ \
--python_out=. \
--grpc_python_out=. \
../proto/twwhatever/demo/*.proto
That way I got a package twwhatever.demo and I could use
from twwhatever.demo import service_pb2_grpc
from twwhatever.demo import message_pb2
It appears that the same strategy works if you add
additional submodule structure (e.g., twwhatever/demo/mypackage, etc.).
The takeaway seems to be to put all the protocol buffer definitions
for a project under the same directory structure, and possibly better
yet in the same .proto file.