From e036eadb24a68410dd98e4bc1e499d4ef455dd98 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Mon, 9 Feb 2026 10:00:04 -0500 Subject: [PATCH 1/4] Mark plugin as supporting edition 2024 Ref: https://github.com/connectrpc/connect-python/pull/119#discussion_r2776064390 Signed-off-by: Stefan VanBuren --- README.md | 4 ++-- protoc-gen-connect-python/generator/generator.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ccab6b2..d2af870 100644 --- a/README.md +++ b/README.md @@ -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: ```proto -edition = "2023"; +edition = "2024"; package your.service; diff --git a/protoc-gen-connect-python/generator/generator.go b/protoc-gen-connect-python/generator/generator.go index 7f5a3a6..eaca278 100644 --- a/protoc-gen-connect-python/generator/generator.go +++ b/protoc-gen-connect-python/generator/generator.go @@ -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, ) conf := parseConfig(request.Parameter()) From db6c3c6880c7826c6cff9feb92920d0985efee1f Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Mon, 9 Feb 2026 10:06:31 -0500 Subject: [PATCH 2/4] Fix test We could test 2023 as well, but seems fine to just bump to 2024. Signed-off-by: Stefan VanBuren --- .../generator/generator_test.go | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/protoc-gen-connect-python/generator/generator_test.go b/protoc-gen-connect-python/generator/generator_test.go index 3bac8ad..bc1602d 100644 --- a/protoc-gen-connect-python/generator/generator_test.go +++ b/protoc-gen-connect-python/generator/generator_test.go @@ -222,20 +222,20 @@ 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 + // Create a request with an Edition 2024 proto file + edition2024 := descriptorpb.Edition_EDITION_2024 req := &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{"test_edition2023.proto"}, + FileToGenerate: []string{"test_edition2024.proto"}, ProtoFile: []*descriptorpb.FileDescriptorProto{ { - Name: proto.String("test_edition2023.proto"), - Package: proto.String("test.edition2023"), - Edition: edition2023.Enum(), - // Edition 2023 default: field_presence = EXPLICIT + Name: proto.String("test_edition2024.proto"), + Package: proto.String("test.edition2024"), + Edition: edition2024.Enum(), + // Edition 2024 default: field_presence = EXPLICIT Options: &descriptorpb.FileOptions{ Features: &descriptorpb.FeatureSet{ FieldPresence: descriptorpb.FeatureSet_EXPLICIT.Enum(), @@ -243,12 +243,12 @@ func TestEdition2023Support(t *testing.T) { }, Service: []*descriptorpb.ServiceDescriptorProto{ { - Name: proto.String("Edition2023Service"), + Name: proto.String("Edition2024Service"), Method: []*descriptorpb.MethodDescriptorProto{ { Name: proto.String("TestMethod"), - InputType: proto.String(".test.edition2023.TestRequest"), - OutputType: proto.String(".test.edition2023.TestResponse"), + InputType: proto.String(".test.edition2024.TestRequest"), + OutputType: proto.String(".test.edition2024.TestResponse"), }, }, }, @@ -262,7 +262,7 @@ func TestEdition2023Support(t *testing.T) { 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 + // In Edition 2024, field presence is controlled by features }, }, }, @@ -287,7 +287,7 @@ func TestEdition2023Support(t *testing.T) { // Verify no error occurred if resp.GetError() != "" { - t.Fatalf("generate() failed for Edition 2023 proto: %v", resp.GetError()) + t.Fatalf("generate() failed for Edition 2024 proto: %v", resp.GetError()) } // Verify the generator declared Edition support @@ -299,23 +299,23 @@ func TestEdition2023Support(t *testing.T) { 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.GetMaximumEdition() != int32(descriptorpb.Edition_EDITION_2024) { + t.Errorf("Expected maximum edition 2024, got %v", resp.GetMaximumEdition()) } // Verify a file was generated if len(resp.GetFile()) == 0 { - t.Error("No files generated for Edition 2023 proto") + t.Error("No files generated for Edition 2024 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 generatedFile.GetName() != "test_edition2024_connect.py" { + t.Errorf("Expected filename test_edition2024_connect.py, got %v", generatedFile.GetName()) } // Verify the generated content includes the service content := generatedFile.GetContent() - if !strings.Contains(content, "class Edition2023Service") { - t.Error("Generated code missing Edition2023Service class") + if !strings.Contains(content, "class Edition2024Service") { + t.Error("Generated code missing Edition2024Service class") } } } From 56907891b8a71a3c6f360a3693555f534dc3ad01 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Tue, 10 Feb 2026 08:20:56 -0500 Subject: [PATCH 3/4] Revert test back to 2023 Signed-off-by: Stefan VanBuren --- .../generator/generator_test.go | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/protoc-gen-connect-python/generator/generator_test.go b/protoc-gen-connect-python/generator/generator_test.go index bc1602d..566404e 100644 --- a/protoc-gen-connect-python/generator/generator_test.go +++ b/protoc-gen-connect-python/generator/generator_test.go @@ -225,17 +225,17 @@ func TestGenerate(t *testing.T) { func TestEditionSupport(t *testing.T) { t.Parallel() - // Create a request with an Edition 2024 proto file - edition2024 := descriptorpb.Edition_EDITION_2024 + // Create a request with an Edition 2023 proto file + edition2023 := descriptorpb.Edition_EDITION_2023 req := &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{"test_edition2024.proto"}, + FileToGenerate: []string{"test_edition2023.proto"}, ProtoFile: []*descriptorpb.FileDescriptorProto{ { - Name: proto.String("test_edition2024.proto"), - Package: proto.String("test.edition2024"), - Edition: edition2024.Enum(), - // Edition 2024 default: field_presence = EXPLICIT + 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(), @@ -243,12 +243,12 @@ func TestEditionSupport(t *testing.T) { }, Service: []*descriptorpb.ServiceDescriptorProto{ { - Name: proto.String("Edition2024Service"), + Name: proto.String("Edition2023Service"), Method: []*descriptorpb.MethodDescriptorProto{ { Name: proto.String("TestMethod"), - InputType: proto.String(".test.edition2024.TestRequest"), - OutputType: proto.String(".test.edition2024.TestResponse"), + InputType: proto.String(".test.edition2023.TestRequest"), + OutputType: proto.String(".test.edition2023.TestResponse"), }, }, }, @@ -262,7 +262,7 @@ func TestEditionSupport(t *testing.T) { Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), - // In Edition 2024, field presence is controlled by features + // In Edition 2023, field presence is controlled by features }, }, }, @@ -287,7 +287,7 @@ func TestEditionSupport(t *testing.T) { // Verify no error occurred if resp.GetError() != "" { - t.Fatalf("generate() failed for Edition 2024 proto: %v", resp.GetError()) + t.Fatalf("generate() failed for Edition 2023 proto: %v", resp.GetError()) } // Verify the generator declared Edition support @@ -299,23 +299,23 @@ func TestEditionSupport(t *testing.T) { 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_2024) { - t.Errorf("Expected maximum edition 2024, got %v", resp.GetMaximumEdition()) + if resp.GetMaximumEdition() != int32(descriptorpb.Edition_EDITION_2023) { + t.Errorf("Expected maximum edition 2023, got %v", resp.GetMaximumEdition()) } // Verify a file was generated if len(resp.GetFile()) == 0 { - t.Error("No files generated for Edition 2024 proto") + t.Error("No files generated for Edition 2023 proto") } else { generatedFile := resp.GetFile()[0] - if generatedFile.GetName() != "test_edition2024_connect.py" { - t.Errorf("Expected filename test_edition2024_connect.py, got %v", generatedFile.GetName()) + if generatedFile.GetName() != "test_edition2023_connect.py" { + t.Errorf("Expected filename test_edition2023_connect.py, got %v", generatedFile.GetName()) } // Verify the generated content includes the service content := generatedFile.GetContent() - if !strings.Contains(content, "class Edition2024Service") { - t.Error("Generated code missing Edition2024Service class") + if !strings.Contains(content, "class Edition2023Service") { + t.Error("Generated code missing Edition2023Service class") } } } From 91a76ccff78f0f7d7c5b7cde8fa52409c6fe1cd9 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Tue, 10 Feb 2026 08:25:34 -0500 Subject: [PATCH 4/4] Refactor edition test to be table-based May as well test each edition. Also, make all subtests parallel. Signed-off-by: Stefan VanBuren --- .../generator/generator_test.go | 180 +++++++++++------- 1 file changed, 106 insertions(+), 74 deletions(-) diff --git a/protoc-gen-connect-python/generator/generator_test.go b/protoc-gen-connect-python/generator/generator_test.go index 566404e..ff9b2ac 100644 --- a/protoc-gen-connect-python/generator/generator_test.go +++ b/protoc-gen-connect-python/generator/generator_test.go @@ -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) @@ -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() == "" { @@ -225,98 +227,128 @@ func TestGenerate(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) + } + }) } }