From 7302d681b155c6e9a6c7762d767f0071ef5909d7 Mon Sep 17 00:00:00 2001 From: Tobias Sargeant Date: Mon, 17 Aug 2026 22:33:34 +1000 Subject: [PATCH] Python: don't emit ClassVar for a file's top-level extensions PrintExtensions is a template instantiated for both a Descriptor (extensions nested in a message, printed into the class body) and a FileDescriptor (top-level extensions, printed at module scope), and emitted _ClassVar[int] for both. ClassVar is only a valid annotation inside a class body, so a proto declaring a file-level extension produced a .pyi that does not type-check: ext_pb2.pyi:7:1 - error: "ClassVar" is not allowed in this context Give it the is_classvar parameter PrintEnumValues already uses for the same distinction, and pass it only from the message call site. Fixes #29221 --- .../protobuf/compiler/python/pyi_generator.cc | 16 ++++++++++++---- .../protobuf/compiler/python/pyi_generator.h | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/google/protobuf/compiler/python/pyi_generator.cc b/src/google/protobuf/compiler/python/pyi_generator.cc index 541bec0aa91b4..739bf5a7b347e 100644 --- a/src/google/protobuf/compiler/python/pyi_generator.cc +++ b/src/google/protobuf/compiler/python/pyi_generator.cc @@ -361,14 +361,22 @@ void PyiGenerator::PrintTopLevelEnums() const { } template -void PyiGenerator::PrintExtensions(const DescriptorT& descriptor) const { +void PyiGenerator::PrintExtensions(const DescriptorT& descriptor, + bool is_classvar) const { for (int i = 0; i < descriptor.extension_count(); ++i) { const FieldDescriptor* extension_field = descriptor.extension(i); std::string constant_name = absl::StrCat(extension_field->name(), "_FIELD_NUMBER"); absl::AsciiStrToUpper(&constant_name); - printer_->Print("$constant_name$: _ClassVar[int]\n", - "constant_name", constant_name); + // ClassVar is only a valid annotation inside a class body, so it is used for a message's + // nested extensions and not for a file's top-level ones. + if (is_classvar) { + printer_->Print("$constant_name$: _ClassVar[int]\n", + "constant_name", constant_name); + } else { + printer_->Print("$constant_name$: int\n", + "constant_name", constant_name); + } Annotate("constant_name", extension_field); printer_->Print("$name$: _descriptor.FieldDescriptor\n", "name", extension_field->name()); @@ -478,7 +486,7 @@ void PyiGenerator::PrintMessage(const Descriptor& message_descriptor, PrintMessage(*message_descriptor.nested_type(i), true); } - PrintExtensions(message_descriptor); + PrintExtensions(message_descriptor, /* is_classvar = */ true); // Prints field number for (int i = 0; i < message_descriptor.field_count(); ++i) { diff --git a/src/google/protobuf/compiler/python/pyi_generator.h b/src/google/protobuf/compiler/python/pyi_generator.h index 1beb4d11af39b..adf752c5b6dda 100644 --- a/src/google/protobuf/compiler/python/pyi_generator.h +++ b/src/google/protobuf/compiler/python/pyi_generator.h @@ -73,7 +73,8 @@ class PROTOC_EXPORT PyiGenerator : public google::protobuf::compiler::CodeGenera void PrintEnumValues(const EnumDescriptor& enum_descriptor, bool is_classvar = false) const; template - void PrintExtensions(const DescriptorT& descriptor) const; + void PrintExtensions(const DescriptorT& descriptor, + bool is_classvar = false) const; void PrintMessages() const; void PrintMessage(const Descriptor& message_descriptor, bool is_nested) const; void PrintServices() const;