Skip to content

Commit 53d3a31

Browse files
committed
docs: restructure and add more samples
1 parent d491877 commit 53d3a31

File tree

11 files changed

+333
-4
lines changed

11 files changed

+333
-4
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
output_root
2+
/acme/
3+
14
testout/
25
.vscode/
36
.vim/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# protoc-gen-dataclasses sample plugin
2+
3+
This sample demonstrates the use of `py_import_func` and shows how to signal support for and handle proto3 optionals.
4+
5+
## Run
6+
7+
Ensure a folder called `output_root` exists:
8+
9+
```
10+
mkdir output_root
11+
rm -r output_root/acme
12+
```
13+
14+
generate
15+
16+
```sh
17+
protoc \
18+
--plugin=protoc-gen-dataclasses=samples/protoc-gen-dataclasses/plugin.py \
19+
--dataclasses_out=output_root \
20+
--proto_path samples/protos \
21+
samples/protos/acme/**/*.proto
22+
``
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python
2+
3+
import protogen
4+
import google.protobuf.compiler.plugin_pb2
5+
6+
dataclassesPackage = protogen.PyImportPath("dataclasses")
7+
8+
9+
def generate(gen: protogen.Plugin):
10+
for f in gen.files_to_generate:
11+
# This plugin generates a file with suffix `_dataclasses.py` file for
12+
# each `.proto' file to generate. For a proto file "a/b/library.proto"
13+
# a python file "a/b/library_dataclasses.py" will be generated. The
14+
# python module name of this file is "a.b.library_dataclasses". This is
15+
# different from the behaviour assumed by default, which would generate
16+
# a "a/b/library_pb2.py" file that has a module name of "a.b.library_pb2".
17+
#
18+
# This changed module naming behaviour changes the way messages and need
19+
# to be imported between two python files: instead of importing the
20+
# "a.b.library" module when a message from the "a/b/library.proto" file
21+
# is referenced, the "a.b.library_dataclasses" module needs to be
22+
# imported.
23+
#
24+
# To import the correct modules, the python identifiers of the messages
25+
# the plugin has to work with (see `protogen.Message.py_ident`) must be
26+
# adjusted accordingly. This is done by providing the
27+
# `datalasses_py_import_func` to the `protogen.Options` below. With that
28+
# all messages will use the
29+
#
30+
# ```
31+
# proto_filename.replace(".proto", "_dataclasses").replace("/", ".")
32+
# ```
33+
#
34+
# as the python import path.
35+
g = gen.new_generated_file(
36+
f.proto.name.replace(".proto", "_dataclasses.py"),
37+
f.py_import_path,
38+
)
39+
g.P("# Autogenerated code. DO NOT EDIT.")
40+
g.P(f'"""This is an module docstring."""')
41+
g.P()
42+
g.print_import()
43+
g.P()
44+
45+
for message in f.messages:
46+
g.P("@", dataclassesPackage.ident("dataclass"))
47+
g.P(f"class {message.proto.name}:")
48+
g.P(f' """{message.location.leading_comments}"""')
49+
g.P()
50+
51+
for field in message.fields:
52+
if field.kind == protogen.Kind.MESSAGE:
53+
g.P(" ", field.py_name, ": ", field.message.py_ident)
54+
elif field.proto.proto3_optional:
55+
g.P(f" {field.py_name}: str | None") # use str for simplicity
56+
else:
57+
g.P(f" {field.py_name}: str")
58+
59+
g.P()
60+
61+
62+
def dataclasses_py_import_func(proto_filename: str, proto_package: str):
63+
return protogen.PyImportPath(
64+
proto_filename.replace(".proto", "_dataclasses").replace("/", ".")
65+
)
66+
67+
68+
opts = protogen.Options(
69+
py_import_func=dataclasses_py_import_func,
70+
# To indicate to be able to handle proto3 optionals a protoc plugin must set
71+
# `google.protobuf.compiler.plugin_pb2.CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL`
72+
# in the `supported_features` list. This enum value will be delegated to
73+
# protoc via the CodeGeneratorResponse.supported_features field.
74+
# See also https://github.com/protocolbuffers/protobuf/blob/fe0a809d5ef4/src/google/protobuf/compiler/plugin.proto#L107-L109).
75+
supported_features=[
76+
google.protobuf.compiler.plugin_pb2.CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL
77+
],
78+
)
79+
opts.run(generate)

samples/protoc-gen-empty/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# protoc-gen-empty sample
2+
3+
This is a basic example of a protoc plugin.
4+
5+
## Run
6+
7+
Ensure a folder called `output_root` exists:
8+
9+
```
10+
mkdir output_root
11+
rm -r output_root/acme
12+
```
13+
14+
Generate code for the `samples/protos` protobuf definitions with the protoc-gen-empty plugin:
15+
16+
```sh
17+
protoc \
18+
--plugin=protoc-gen-empty=samples/protoc-gen-empty/plugin.py \
19+
--empty_out=output_root \
20+
--proto_path samples/protos \
21+
samples/protos/acme/**/*.proto
22+
``

samples/empty/__init__.py renamed to samples/protoc-gen-empty/plugin.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def generate(gen: protogen.Plugin):
99
for f in gen.files_to_generate:
1010
if f.generate:
1111
g = gen.new_generated_file(
12-
f.proto.name.replace(".proto", ".py"),
12+
f.proto.name.replace(".proto", "_pb2.py"),
1313
f.py_import_path,
1414
)
1515
g.P("# Autogenerated code. DO NOT EDIT.")
@@ -19,19 +19,19 @@ def generate(gen: protogen.Plugin):
1919
g.P()
2020

2121
for enum in f.enums:
22-
g.P(f"class {enum.py_ident.py_name}(", enumPackage.ident("Enum"), "):")
22+
g.P(f"class {enum.proto.name}(", enumPackage.ident("Enum"), "):")
2323
for value in enum.values:
2424
g.P(f" {value.proto.name}={value.number}")
2525
g.P()
2626

2727
for message in f.messages:
28-
g.P(f"class {message.py_ident.py_name}:")
28+
g.P(f"class {message.proto.name}:")
2929
g.P(f" def __init__(self, host: str):")
3030
g.P(f" self.host = host")
3131
g.P()
3232

3333
for service in f.services:
34-
g.P(f"class {service.py_ident.py_name}Client:")
34+
g.P(f"class {service.proto.name}Client:")
3535
g.P(f" def __init__(self, host: str):")
3636
g.P(f" self.host = host")
3737
g.P()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# protoc-gen-extensions sample
2+
3+
This sample shows how to use proto3 extensions (`MethodOptions` more specifically) and the `protogen.Registry` to resolve messages.
4+
5+
## Run
6+
7+
For a plugin that uses extensions the extension needs to be generated first using the official protoc python compiler.
8+
This is necessary for the corresponding options that use e.g. in `samples/protos/acme/library/v1/library.proto` to be present and accessible for the example plugin.
9+
Generate python code for at least the file where the extension resides in and all its dependencies with the official Python protoc plugin.
10+
11+
```sh
12+
protoc --proto_path=samples/protos --python_out=. samples/protos/acme/longrunning/operations.proto samples/protos/acme/protobuf/any.proto
13+
``
14+
15+
Ensure a folder called `output_root` exists:
16+
17+
```
18+
mkdir output_root
19+
rm -r output_root/acme
20+
```
21+
22+
Then run plugin.
23+
24+
```sh
25+
protoc --plugin=protoc-gen-extensions=samples/protoc-gen-extensions/plugin.py --extensions_out=output_root -I samples/protos samples/protos/acme/**/*.proto
26+
``
27+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python
2+
3+
from dataclasses import dataclass
4+
from typing import Optional
5+
import protogen
6+
7+
# Import the protobuf definitions that have been generated with the official
8+
# protobuf plugin for python.
9+
import acme.longrunning.operations_pb2
10+
11+
12+
@dataclass
13+
class OperationInfo:
14+
response_type: protogen.Message
15+
metadata_type: protogen.Message
16+
17+
18+
def operation_info(
19+
registry: protogen.Registry, method: protogen.Method
20+
) -> Optional[OperationInfo]:
21+
# Any options set for the extension can be found in the `options` field of
22+
# the protobuf message they have been set on, here on the `Method`.
23+
pb = None
24+
for fd, msg in method.proto.options.ListFields():
25+
if fd.number == acme.longrunning.operations_pb2.OPERATION_INFO_FIELD_NUMBER:
26+
pb = msg
27+
break
28+
29+
if pb is None:
30+
# The extension has not been set on the method.
31+
return None
32+
33+
# pb is at this point a protobuf message as generated by the offical
34+
# protobuf plugin for python. In the example the extensions message type is
35+
# `operations_pb2.OperationInfo`.
36+
#
37+
# `operations_pb2.OperationInfo` has two fields: `response_type` and
38+
# `metadata_type` which themselfes reference protobuf messages by their
39+
# name. These names might be fully qualified like "google.protobuf.Empty" or
40+
# not fully qualified like `WriteBookRequest` which would resolve to
41+
# `acme.library.v1.WritebookRequest` given the current scope is the
42+
# `acme.library-v1.Library.WriteBook` service method. To resolve non
43+
# fully-qualified message names given a current scope as reference, use
44+
# `protogen.Registry.resolve_message_type`.
45+
46+
operation_info = OperationInfo(
47+
registry.resolve_message_type(method.full_name, pb.response_type),
48+
registry.resolve_message_type(method.full_name, pb.metadata_type),
49+
)
50+
51+
if operation_info.response_type is None:
52+
raise Exception(
53+
f"message {pb.response_type} could not be resolved from base {method.full_name}"
54+
)
55+
56+
if operation_info.metadata_type is None:
57+
raise Exception(
58+
f"message {pb.metadata_type} could not be resolved from base {method.full_name}"
59+
)
60+
61+
return operation_info
62+
63+
64+
def generate(gen: protogen.Plugin):
65+
for f in gen.files_to_generate:
66+
g = gen.new_generated_file(
67+
f.proto.name.replace(".proto", "_ext.py"),
68+
f.py_import_path,
69+
)
70+
71+
g.P("# Autogenerated code. DO NOT EDIT.")
72+
g.P(f'"""This is an module docstring."""')
73+
g.P()
74+
g.print_import()
75+
g.P()
76+
77+
for service in f.services:
78+
g.P(f"class {service.py_ident.py_name}Client:")
79+
g.P(f" def __init__(self, host: str):")
80+
g.P(f" self.host = host")
81+
g.P()
82+
83+
for method in service.methods:
84+
# Create a new class `<MethodName>Operation` for each response type
85+
# for each method that returns a acme.longrunning.Operation.
86+
if method.output.full_name == "acme.longrunning.Operation":
87+
op = operation_info(gen.registry, method)
88+
g.P(f"class {method.proto.name}Operation:")
89+
g.P(" def wait() -> ", method.output.py_ident, ":")
90+
g.P(" pass")
91+
g.P()
92+
g.P(" response_type:", op.response_type.py_ident)
93+
g.P(" metadata_type:", op.metadata_type.py_ident)
94+
95+
# fmt: off
96+
g.P(f" def {method.py_name}(req: ", method.input.py_ident, ") -> ", method.output.py_ident, ":")
97+
g.P(f" pass")
98+
g.P()
99+
# fmt: on
100+
101+
102+
def options_py_import_func(proto_filename: str, proto_package: str):
103+
return protogen.PyImportPath(
104+
proto_filename.replace(".proto", "_ext").replace("/", ".")
105+
)
106+
107+
108+
opts = protogen.Options(py_import_func=options_py_import_func)
109+
opts.run(generate)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
syntax = "proto3";
2+
3+
package acme.library.v1;
4+
5+
import "acme/longrunning/operations.proto";
6+
7+
// Note that this import is not actively used in this proto file. However the
8+
// `metadata_type` of this extension references the `acme.protobuf.Empty` type
9+
// and it should be resolved and used by the plugin.
10+
import "acme/protobuf/empty.proto";
11+
12+
service Library {
13+
14+
rpc WriteBook(WriteBookRequest) returns (acme.longrunning.Operation) {
15+
option (acme.longrunning.operation_info) = {
16+
response_type : "WriteBookResponse"
17+
metadata_type : "acme.protobuf.Empty"
18+
};
19+
}
20+
}
21+
22+
message Book { string name = 1; }
23+
24+
message WriteBookRequest { string random_field = 1; }
25+
26+
message WriteBookResponse { string random_field = 1; }
27+
28+
message WriteBookMetadata { string random_field = 1; }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
syntax = "proto3";
2+
3+
package acme.longrunning;
4+
5+
import "acme/protobuf/any.proto";
6+
import "google/protobuf/descriptor.proto";
7+
8+
extend google.protobuf.MethodOptions { OperationInfo operation_info = 1049; }
9+
10+
message Operation {
11+
string name = 1;
12+
13+
acme.protobuf.Any metadata = 2;
14+
15+
bool done = 3;
16+
17+
oneof result {
18+
string error = 4;
19+
acme.protobuf.Any response = 5;
20+
}
21+
}
22+
23+
message OperationInfo {
24+
string response_type = 1;
25+
string metadata_type = 2;
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
syntax = "proto3";
2+
3+
package acme.protobuf;
4+
5+
message Any {
6+
string type_url = 1;
7+
bytes value = 2;
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
syntax = "proto3";
2+
3+
package acme.protobuf;
4+
5+
message Empty {}

0 commit comments

Comments
 (0)