gRPC Pythonのコード生成で、相対参照に近い意図したパッケージとして生成する

PythonでgRPCを使う場合には、grpcio-toolsパッケージを使いますが、この使い方で目的のパッケージの中にあるコードとして生成しようとした時に、ハマったので解説します。

## 実現したいこと

用意した定義ファイルはこちら

```proto:proto/gisapp.proto
syntax = "proto3";

package proto;

service GISCalc {
  rpc RouteLength(RouteLengthRequest) returns (RouteLengthResponse) {}
}

message Point {
  double latitude = 1;
  double lognitude = 2;
}

message RouteLengthRequest { repeated Point route = 1; }

message RouteLengthResponse { double length = 1; }

```

ここから、以下のように使えるように `proto` パッケージの中に生成したコードを収めたいとします。

```python:test.py
import grpc
from proto import gisapp_pb2_grpc, gisapp_pb2

channel = grpc.insecure_channel('localhost:50051')
client = gisapp_pb2_grpc.GISCalcStub(channel)
req = gisapp_pb2.RouteLengthRequest()
res = client.RouteLength()
```

つまり、以下のようなディレクトリ構成にします。

```
`- proto
   |- gisapp_pb2.py
   `- gisapp_pb2_grpc.py
```

## どうすればよいか

パッケージにしたいディレクトリの中に、定義ファイルを置きます。

```
`- proto
   `- gisapp.proto
```

ルートディレクトリにて、以下のように実行します。

```sh
python -m grpc_tools.protoc \
    -I=. \
    --python_out=. \
    --grpc_python_out=. \
    proto/gisapp.proto
```

すると、以下のような、`proto`パッケージ下にあるプログラムとして生成されます。

```python:proto/gisapp_pb_grpc.py
import grpc

from proto import gisapp_pb2 as proto_dot_gisapp__pb2

...
```

## 解説

パッケージ構成は、`-I`で指定したフォルダから、定義ファイル `gisapp.proto` までのディレクトリ構成になります。以下のような関係になります。

- `-I./proto proto/gisapp.proto` →`import gisapp_pb2` 
- `-I. proto/gisapp.proto` →`from proto import gisapp_pb2` 

次に、出力先に引数である `--python_out`、`--grpc_python_out` は、指定したパスをルートとするパッケージとして出力されます。

- `-I./proto proto/gisapp.proto`かつ
    - `--python_out=./proto` → `proto/proto/gisapp_pb2.py` に `import gisapp_pb2` が作られる
    - `--python_out=.` → `proto/gisapp_pb2.py` に `import gisapp_pb2`
- `-I. proto/gisapp.proto` かつ
    - `--python_out=./proto` → `proto/proto/gisapp_pb2.py` に `from proto import gisapp_pb2` が作られる
    - `--python_out=.` → `proto/gisapp_pb2.py` に `from proto import gisapp_pb2` が作られる

よって、前述のコマンドのとおりになります。

## ところで

betterproto というツールで生成したところ、どこのパッケージ下にもおける1ファイルが生成されるようになったので、上記のようなパッケージの問題は消失しました。

https://github.com/danielgtaylor/python-betterproto

```
pip install betterproto[compiler]==2.0.0b3

python -m grpc_tools.protoc \
    -I=. \
    --python_betterproto_out=. \
    proto/gisapp.proto
```

```python:proto/__init__.py
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# sources: proto/gisapp.proto
# plugin: python-betterproto
from dataclasses import dataclass
from typing import Dict, List, Optional

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


@dataclass(eq=False, repr=False)
class Point(betterproto.Message):
    latitude: float = betterproto.double_field(1)
    longitude: float = betterproto.double_field(2)


@dataclass(eq=False, repr=False)
class RouteLengthRequest(betterproto.Message):
    route: List["Point"] = betterproto.message_field(1)


@dataclass(eq=False, repr=False)
class RouteLengthResponse(betterproto.Message):
    length: float = betterproto.double_field(1)


class GisCalcStub(betterproto.ServiceStub):
    async def route_length(
        self, *, route: Optional[List["Point"]] = None
    ) -> "RouteLengthResponse":
        ...


class GisCalcBase(ServiceBase):
    async def route_length(
        self, route: Optional[List["Point"]]
    ) -> "RouteLengthResponse":
        ...
```