danielgtaylor / python-betterproto

Clean, modern, Python 3.6+ code generator & library for Protobuf 3 and async gRPC
MIT License
1.51k stars 214 forks source link

Imports from adjacent .proto files does not work #441

Open BananaLoaf opened 1 year ago

BananaLoaf commented 1 year ago

First of all, I looked at the issue #408 and I am absolutely sure that I have betterproto[compiler] of version 2.0.0b5 installed

I have this file structure:

protos
├── common.proto
└── devices
    └── rpi.proto

With protos/common.proto:

syntax = 'proto3';
package org.company.name.common;

message DeviceId {
  string device_id = 1;
}

and protos/devices/rpi.proto:

syntax = 'proto3';
package org.company.name.rpi;

import "common.proto";
import "google/protobuf/empty.proto";

service RPI {
  rpc Start(common.DeviceId) returns (google.protobuf.Empty) {}
  rpc Stop(common.DeviceId) returns (google.protobuf.Empty) {}

I then generate betterproto files:

poetry run python -m grpc_tools.protoc \
        -I protos \
        --python_betterproto_out=package_name/grpc \
        protos/common.proto \
        protos/devices/rpi.proto \

The BUG

In package_name.grpc.org.company.name.rpi any mentions of DeviceId are presented as _common__.DeviceId, with _common__ not being imported. Launching that code does not work either, python has no idea what _common__ is.

JulianNeuberger commented 1 year ago

I stumbled over this problem today and was able to reproduce it in version 2.0.0b6

Given the two proto files below:

dependency.proto

syntax = "proto3";

package report.dependency;

message DependencyMessage {}

main.proto

syntax = "proto3";

import "dependency.proto";

package report.main;

message ReturnMessage {}

service MainService {
  rpc SomeCall(report.dependency.DependencyMessage) returns (ReturnMessage);
}

Running python -m grpc_tools.protoc --proto_path . --python_betterproto_out=. main.proto results in the following, broken python code:

from dataclasses import dataclass
from typing import (
    TYPE_CHECKING,
    Dict,
    Optional,
)

import betterproto
import grpclib
from betterproto.grpc.grpclib_server import ServiceBase

if TYPE_CHECKING:
    import grpclib.server
    from betterproto.grpc.grpclib_client import MetadataLike
    from grpclib.metadata import Deadline

@dataclass(eq=False, repr=False)
class ReturnMessage(betterproto.Message):
    pass

class MainServiceStub(betterproto.ServiceStub):
    async def some_call(
        self,
        dependency_dependency_message: "_dependency__.DependencyMessage",
        *,
        timeout: Optional[float] = None,
        deadline: Optional["Deadline"] = None,
        metadata: Optional["MetadataLike"] = None
    ) -> "ReturnMessage":
        return await self._unary_unary(
            "/report.main.MainService/SomeCall",
            dependency_dependency_message,
            ReturnMessage,
            timeout=timeout,
            deadline=deadline,
            metadata=metadata,
        )

class MainServiceBase(ServiceBase):
    async def some_call(
        self, dependency_dependency_message: "_dependency__.DependencyMessage"
    ) -> "ReturnMessage":
        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)

    async def __rpc_some_call(
        self,
        stream: "grpclib.server.Stream[_dependency__.DependencyMessage, ReturnMessage]",
    ) -> None:
        request = await stream.recv_message()
        response = await self.some_call(request)
        await stream.send_message(response)

    def __mapping__(self) -> Dict[str, grpclib.const.Handler]:
        return {
            "/report.main.MainService/SomeCall": grpclib.const.Handler(
                self.__rpc_some_call,
                grpclib.const.Cardinality.UNARY_UNARY,
                _dependency__.DependencyMessage,
                ReturnMessage,
            ),
        }

Note the missing import from .. import dependency as _dependency__ which would be needed in lines 32, 50, 56, and 67.

Using the message DependencyMessage as a output type generates the import as expected, i.e. the following proto file does not show the problem described above

main.proto

syntax = "proto3";

import "dependency.proto";

package report.main;

message ReturnMessage {}

service MainService {
  rpc SomeCall(report.dependency.DependencyMessage) returns (report.dependency.DependencyMessage);
}

My unsophisticated fix would be to add this line to the top of plugin.models.ServiceMethodCompiler.__post_init:

self.py_input_message_type

Simply using the property seems to fix the problem, as the underlying method for building the import statements adds the needed imports to output_file.imports, which in turn makes them generate properly. This is why using the imported Message type as a return type generates the import properly: we use the property self.py_output_message_type in the check of line 726

I'm sure there is a better way though :)

BananaLoaf commented 1 year ago

@JulianNeuberger I ended up forking it

https://github.com/BananaLoaf/python-bananaproto

I fixed this issue and changed some stuff I needed to change