-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom protoc plugin to generate a grpclib wrapper #2181
Merged
Merged
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e61803f
Adds custom proto generator plugin
freider 19e3490
Replace all usages
freider 911f2f2
Plugin
freider 926d3ba
Move plugin out of modal
freider 098ffab
Don't pollute client cli namespace
freider 492485c
Don't install plugin, just reference script
freider 8d5b26a
Merge remote-tracking branch 'origin/main' into freider/grpc-plugin
freider 858dcf4
Clean up generation
freider 823a992
oops
freider 608d740
Wrapper gen in ci
freider 60c8ac2
Install grpclib before generating proto stubs
freider 8686bf6
Copyright
freider d8a34cd
windows
freider e171dea
no pause
freider a4c4962
Remove prints
freider 3d1c41e
Cleanup task
freider 96b2370
Fix multi-file-access on windows
freider File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +0,0 @@ | ||
# Copyright Modal Labs 2022 | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
#!/usr/bin/env python | ||
# built by modifying grpclib.plugin.main, see https://github.com/vmagamedov/grpclib | ||
# original: Copyright (c) 2019 , Vladimir Magamedov | ||
import os | ||
import sys | ||
from collections import deque | ||
from contextlib import contextmanager | ||
from pathlib import Path | ||
from typing import Any, Collection, Deque, Dict, Iterator, List, NamedTuple, Optional, Tuple | ||
|
||
from google.protobuf.compiler.plugin_pb2 import CodeGeneratorRequest, CodeGeneratorResponse | ||
from google.protobuf.descriptor_pb2 import DescriptorProto, FileDescriptorProto | ||
from grpclib import const | ||
|
||
_CARDINALITY = { | ||
(False, False): const.Cardinality.UNARY_UNARY, | ||
(True, False): const.Cardinality.STREAM_UNARY, | ||
(False, True): const.Cardinality.UNARY_STREAM, | ||
(True, True): const.Cardinality.STREAM_STREAM, | ||
} | ||
|
||
|
||
class Method(NamedTuple): | ||
name: str | ||
cardinality: const.Cardinality | ||
request_type: str | ||
reply_type: str | ||
|
||
|
||
class Service(NamedTuple): | ||
name: str | ||
methods: List[Method] | ||
|
||
|
||
class Buffer: | ||
def __init__(self) -> None: | ||
self._lines: List[str] = [] | ||
self._indent = 0 | ||
|
||
def add(self, string: str, *args: Any, **kwargs: Any) -> None: | ||
line = " " * self._indent * 4 + string.format(*args, **kwargs) | ||
self._lines.append(line.rstrip(" ")) | ||
|
||
@contextmanager | ||
def indent(self) -> Iterator[None]: | ||
self._indent += 1 | ||
try: | ||
yield | ||
finally: | ||
self._indent -= 1 | ||
|
||
def content(self) -> str: | ||
return "\n".join(self._lines) + "\n" | ||
|
||
|
||
def render( | ||
proto_file: str, | ||
imports: Collection[str], | ||
services: Collection[Service], | ||
grpclib_module: str, | ||
) -> str: | ||
buf = Buffer() | ||
buf.add("# Generated by the Modal Protocol Buffers compiler. DO NOT EDIT!") | ||
buf.add("# source: {}", proto_file) | ||
buf.add("# plugin: {}", __name__) | ||
if not services: | ||
return buf.content() | ||
|
||
buf.add("") | ||
for mod in imports: | ||
buf.add("import {}", mod) | ||
for service in services: | ||
buf.add("") | ||
buf.add("") | ||
grpclib_stub_name = f"{service.name}Stub" | ||
buf.add("class {}Modal:", service.name) | ||
with buf.indent(): | ||
buf.add("") | ||
buf.add("def __init__(self, grpclib_stub: {}.{}) -> None:".format(grpclib_module, grpclib_stub_name)) | ||
with buf.indent(): | ||
if len(service.methods) == 0: | ||
buf.add("pass") | ||
for method in service.methods: | ||
name, cardinality, request_type, reply_type = method | ||
wrapper_cls: type | ||
if cardinality is const.Cardinality.UNARY_UNARY: | ||
wrapper_cls = "modal._utils.grpc_utils.UnaryUnaryWrapper" | ||
elif cardinality is const.Cardinality.UNARY_STREAM: | ||
wrapper_cls = "modal._utils.grpc_utils.UnaryStreamWrapper" | ||
# elif cardinality is const.Cardinality.STREAM_UNARY: | ||
# wrapper_cls = StreamUnaryWrapper | ||
# elif cardinality is const.Cardinality.STREAM_STREAM: | ||
# wrapper_cls = StreamStreamWrapper | ||
else: | ||
raise TypeError(cardinality) | ||
|
||
original_method = f"grpclib_stub.{name}" | ||
buf.add(f"self.{name} = {wrapper_cls}({original_method})") | ||
|
||
return buf.content() | ||
|
||
|
||
def _get_proto(request: CodeGeneratorRequest, name: str) -> FileDescriptorProto: | ||
return next(f for f in request.proto_file if f.name == name) | ||
|
||
|
||
def _strip_proto(proto_file_path: str) -> str: | ||
for suffix in [".protodevel", ".proto"]: | ||
if proto_file_path.endswith(suffix): | ||
return proto_file_path[: -len(suffix)] | ||
|
||
return proto_file_path | ||
|
||
|
||
def _base_module_name(proto_file_path: str) -> str: | ||
basename = _strip_proto(proto_file_path) | ||
return basename.replace("-", "_").replace("/", ".") | ||
|
||
|
||
def _proto2pb2_module_name(proto_file_path: str) -> str: | ||
return _base_module_name(proto_file_path) + "_pb2" | ||
|
||
|
||
def _proto2grpc_module_name(proto_file_path: str) -> str: | ||
return _base_module_name(proto_file_path) + "_grpc" | ||
|
||
|
||
def _type_names( | ||
proto_file: FileDescriptorProto, | ||
message_type: DescriptorProto, | ||
parents: Optional[Deque[str]] = None, | ||
) -> Iterator[Tuple[str, str]]: | ||
if parents is None: | ||
parents = deque() | ||
|
||
proto_name_parts = [""] | ||
if proto_file.package: | ||
proto_name_parts.append(proto_file.package) | ||
proto_name_parts.extend(parents) | ||
proto_name_parts.append(message_type.name) | ||
|
||
py_name_parts = [_proto2pb2_module_name(proto_file.name)] | ||
py_name_parts.extend(parents) | ||
py_name_parts.append(message_type.name) | ||
|
||
yield ".".join(proto_name_parts), ".".join(py_name_parts) | ||
|
||
parents.append(message_type.name) | ||
for nested in message_type.nested_type: | ||
yield from _type_names(proto_file, nested, parents=parents) | ||
parents.pop() | ||
|
||
|
||
def main() -> None: | ||
with os.fdopen(sys.stdin.fileno(), "rb") as inp: | ||
request = CodeGeneratorRequest.FromString(inp.read()) | ||
|
||
types_map: Dict[str, str] = {} | ||
for pf in request.proto_file: | ||
for mt in pf.message_type: | ||
types_map.update(_type_names(pf, mt)) | ||
|
||
response = CodeGeneratorResponse() | ||
|
||
# See https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/implementing_proto3_presence.md # noqa | ||
if hasattr(CodeGeneratorResponse, "Feature"): | ||
response.supported_features = CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL | ||
|
||
for file_to_generate in request.file_to_generate: | ||
proto_file = _get_proto(request, file_to_generate) | ||
module_name = _proto2grpc_module_name(file_to_generate) | ||
grpclib_module_path = Path(module_name.replace(".", "/") + ".py") | ||
|
||
imports = ["modal._utils.grpc_utils", module_name] | ||
|
||
services = [] | ||
for service in proto_file.service: | ||
methods = [] | ||
for method in service.method: | ||
cardinality = _CARDINALITY[(method.client_streaming, method.server_streaming)] | ||
methods.append( | ||
Method( | ||
name=method.name, | ||
cardinality=cardinality, | ||
request_type=types_map[method.input_type], | ||
reply_type=types_map[method.output_type], | ||
) | ||
) | ||
services.append(Service(name=service.name, methods=methods)) | ||
|
||
file = response.file.add() | ||
|
||
file.name = str(grpclib_module_path.with_name("modal_" + grpclib_module_path.name)) | ||
file.content = render( | ||
proto_file=proto_file.name, imports=imports, services=services, grpclib_module=module_name | ||
) | ||
|
||
with os.fdopen(sys.stdout.fileno(), "wb") as out: | ||
out.write(response.SerializeToString()) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need
modal._utils.grpc_utils
imported under aTYPE_CHECKING
guard to use a forward reference? Never been totally clear on that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case it's not a forward reference - it's a "real" reference in the generated code and it's added as an import on line 175: https://github.com/modal-labs/modal-client/pull/2181/files#diff-d721170dbb2b36a3f20394f9563415ebe89a0916e4957cf99e7b615cfd8c772fR175
In case of forward/str references I think the TYPE_CHECKING-guarded imports are still required for type checkers to know what it's dealing with
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoops didn't read carefully enough!