Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,10 +383,10 @@ When exceeded, returns `RESOURCE_EXHAUSTED` error.

### Proto Editions Support

connect-python supports Proto Editions 2023:
`protoc-gen-connect-python` supports up to [Protobuf Editions](https://protobuf.dev/editions/overview/) 2024:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seemed more right to mention the plugin here, although I know this goes against the consistency of the rest of the README. 🤷


```proto
edition = "2023";
edition = "2024";

package your.service;

Expand Down
2 changes: 1 addition & 1 deletion protoc-gen-connect-python/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func Handle(ctx context.Context, _ protoplugin.PluginEnv, responseWriter protopl
responseWriter.SetFeatureProto3Optional()
responseWriter.SetFeatureSupportsEditions(
descriptorpb.Edition_EDITION_PROTO3,
descriptorpb.Edition_EDITION_2023,
descriptorpb.Edition_EDITION_2024,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to keep 2023 here too?

Initially I thought the unit test was somewhat overkill but it alerted me to this so it was pretty good I guess :) Maybe we should t.Run across the multiple editions then

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that tripped me up too, but we're actually declaring the min/max "edition" we support here: https://pkg.go.dev/github.com/bufbuild/protoplugin#ResponseWriter. (I don't love the naming of that method, but I guess it's more obvious once you see the argument names.) So 2023 is implicitly supported.

Even so, I took a shot at making the edition tests table-based and tested both 2023 and 2024, just to have coverage: 91a76cc

)

conf := parseConfig(request.Parameter())
Expand Down
182 changes: 107 additions & 75 deletions protoc-gen-connect-python/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func TestGenerateConnectFile(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fd, err := protodesc.NewFile(tt.input, nil)
if err != nil {
t.Fatalf("Failed to create FileDescriptorProto: %v", err)
Expand Down Expand Up @@ -200,6 +201,7 @@ func TestGenerate(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := generate(t, tt.req)
if tt.wantErr {
if resp.GetError() == "" {
Expand All @@ -222,101 +224,131 @@ func TestGenerate(t *testing.T) {
}
}

func TestEdition2023Support(t *testing.T) {
func TestEditionSupport(t *testing.T) {
t.Parallel()

// Create a request with an Edition 2023 proto file
edition2023 := descriptorpb.Edition_EDITION_2023
tests := []struct {
name string
edition descriptorpb.Edition
protoFileName string
packageName string
serviceName string
wantMinEdition descriptorpb.Edition
wantMaxEdition descriptorpb.Edition
wantGeneratedFile string
wantServiceClass string
}{
{
name: "edition 2023",
edition: descriptorpb.Edition_EDITION_2023,
protoFileName: "test_edition2023.proto",
packageName: "test.edition2023",
serviceName: "Edition2023Service",
wantMinEdition: descriptorpb.Edition_EDITION_PROTO3,
wantMaxEdition: descriptorpb.Edition_EDITION_2024,
wantGeneratedFile: "test_edition2023_connect.py",
wantServiceClass: "class Edition2023Service",
},
{
name: "edition 2024",
edition: descriptorpb.Edition_EDITION_2024,
protoFileName: "test_edition2024.proto",
packageName: "test.edition2024",
serviceName: "Edition2024Service",
wantMinEdition: descriptorpb.Edition_EDITION_PROTO3,
wantMaxEdition: descriptorpb.Edition_EDITION_2024,
wantGeneratedFile: "test_edition2024_connect.py",
wantServiceClass: "class Edition2024Service",
},
}

req := &pluginpb.CodeGeneratorRequest{
FileToGenerate: []string{"test_edition2023.proto"},
ProtoFile: []*descriptorpb.FileDescriptorProto{
{
Name: proto.String("test_edition2023.proto"),
Package: proto.String("test.edition2023"),
Edition: edition2023.Enum(),
// Edition 2023 default: field_presence = EXPLICIT
Options: &descriptorpb.FileOptions{
Features: &descriptorpb.FeatureSet{
FieldPresence: descriptorpb.FeatureSet_EXPLICIT.Enum(),
},
},
Service: []*descriptorpb.ServiceDescriptorProto{
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := &pluginpb.CodeGeneratorRequest{
FileToGenerate: []string{tt.protoFileName},
ProtoFile: []*descriptorpb.FileDescriptorProto{
{
Name: proto.String("Edition2023Service"),
Method: []*descriptorpb.MethodDescriptorProto{
{
Name: proto.String("TestMethod"),
InputType: proto.String(".test.edition2023.TestRequest"),
OutputType: proto.String(".test.edition2023.TestResponse"),
Name: proto.String(tt.protoFileName),
Package: proto.String(tt.packageName),
Edition: tt.edition.Enum(),
Options: &descriptorpb.FileOptions{
Features: &descriptorpb.FeatureSet{
FieldPresence: descriptorpb.FeatureSet_EXPLICIT.Enum(),
},
},
},
},
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("TestRequest"),
Field: []*descriptorpb.FieldDescriptorProto{
Service: []*descriptorpb.ServiceDescriptorProto{
{
Name: proto.String("message"),
Number: proto.Int32(1),
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
// In Edition 2023, field presence is controlled by features
Name: proto.String(tt.serviceName),
Method: []*descriptorpb.MethodDescriptorProto{
{
Name: proto.String("TestMethod"),
InputType: proto.String("." + tt.packageName + ".TestRequest"),
OutputType: proto.String("." + tt.packageName + ".TestResponse"),
},
},
},
},
},
{
Name: proto.String("TestResponse"),
Field: []*descriptorpb.FieldDescriptorProto{
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("result"),
Number: proto.Int32(1),
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
Name: proto.String("TestRequest"),
Field: []*descriptorpb.FieldDescriptorProto{
{
Name: proto.String("message"),
Number: proto.Int32(1),
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
},
},
},
{
Name: proto.String("TestResponse"),
Field: []*descriptorpb.FieldDescriptorProto{
{
Name: proto.String("result"),
Number: proto.Int32(1),
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
},
},
},
},
},
},
},
},
}
}

// Call Generate
resp := generate(t, req)
resp := generate(t, req)

// Verify no error occurred
if resp.GetError() != "" {
t.Fatalf("generate() failed for Edition 2023 proto: %v", resp.GetError())
}
if resp.GetError() != "" {
t.Fatalf("generate() failed for %s proto: %v", tt.name, resp.GetError())
}

// Verify the generator declared Edition support
if resp.GetSupportedFeatures()&uint64(pluginpb.CodeGeneratorResponse_FEATURE_SUPPORTS_EDITIONS) == 0 {
t.Error("Generator should declare FEATURE_SUPPORTS_EDITIONS")
}
if resp.GetSupportedFeatures()&uint64(pluginpb.CodeGeneratorResponse_FEATURE_SUPPORTS_EDITIONS) == 0 {
t.Error("Generator should declare FEATURE_SUPPORTS_EDITIONS")
}

// Verify minimum and maximum editions are set
if resp.GetMinimumEdition() != int32(descriptorpb.Edition_EDITION_PROTO3) {
t.Errorf("Expected minimum edition PROTO3, got %v", resp.GetMinimumEdition())
}
if resp.GetMaximumEdition() != int32(descriptorpb.Edition_EDITION_2023) {
t.Errorf("Expected maximum edition 2023, got %v", resp.GetMaximumEdition())
}
if resp.GetMinimumEdition() != int32(tt.wantMinEdition) {
t.Errorf("Expected minimum edition %v, got %v", tt.wantMinEdition, resp.GetMinimumEdition())
}
if resp.GetMaximumEdition() != int32(tt.wantMaxEdition) {
t.Errorf("Expected maximum edition %v, got %v", tt.wantMaxEdition, resp.GetMaximumEdition())
}

// Verify a file was generated
if len(resp.GetFile()) == 0 {
t.Error("No files generated for Edition 2023 proto")
} else {
generatedFile := resp.GetFile()[0]
if generatedFile.GetName() != "test_edition2023_connect.py" {
t.Errorf("Expected filename test_edition2023_connect.py, got %v", generatedFile.GetName())
}
if len(resp.GetFile()) == 0 {
t.Errorf("No files generated for %s proto", tt.name)
return
}

// Verify the generated content includes the service
content := generatedFile.GetContent()
if !strings.Contains(content, "class Edition2023Service") {
t.Error("Generated code missing Edition2023Service class")
}
generatedFile := resp.GetFile()[0]
if generatedFile.GetName() != tt.wantGeneratedFile {
t.Errorf("Expected filename %s, got %v", tt.wantGeneratedFile, generatedFile.GetName())
}

content := generatedFile.GetContent()
if !strings.Contains(content, tt.wantServiceClass) {
t.Errorf("Generated code missing %s", tt.wantServiceClass)
}
})
}
}

Expand Down