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/
- demo.py
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/
- demo.py
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.