What version of protobuf and what language are you using?
Version: libprotoc 33.5 (also present on main)
Language: Python (--pyi_out)
What did you do?
Compile a proto that declares a file-level extension:
syntax = "proto2";
package probe;
import "google/protobuf/descriptor.proto";
// file-level extension -> emitted at MODULE scope
extend google.protobuf.FileOptions {
optional bool top_level_ext = 50001;
}
message Holder {
// nested extension -> emitted inside the class body
extend google.protobuf.MessageOptions {
optional bool nested_ext = 50002;
}
}
protoc --pyi_out=. ext.proto
What did you expect to see?
A .pyi that type-checks.
What did you see instead?
ClassVar is emitted at module scope, where it is not a valid annotation:
DESCRIPTOR: _descriptor.FileDescriptor
TOP_LEVEL_EXT_FIELD_NUMBER: _ClassVar[int] # <-- module scope
top_level_ext: _descriptor.FieldDescriptor
class Holder(_message.Message):
__slots__ = ()
NESTED_EXT_FIELD_NUMBER: _ClassVar[int] # <-- class body, correct
nested_ext: _descriptor.FieldDescriptor
def __init__(self) -> None: ...
ext_pb2.pyi:7:1 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
Only the module-scope one is flagged; the nested one is fine. PEP 526 restricts ClassVar to class bodies.
Cause
PyiGenerator::PrintExtensions (src/google/protobuf/compiler/python/pyi_generator.cc:364) is a template over DescriptorT and emits _ClassVar[int] unconditionally:
template <typename DescriptorT>
void PyiGenerator::PrintExtensions(const DescriptorT& descriptor) const {
...
printer_->Print("$constant_name$: _ClassVar[int]\n",
"constant_name", constant_name);
It is instantiated for both scopes:
| call site |
descriptor |
output scope |
_ClassVar valid |
:481 PrintExtensions(message_descriptor) |
Descriptor |
class body |
yes |
:656 PrintExtensions(*public_dep) |
FileDescriptor |
module |
no |
:669 PrintExtensions(*file_) |
FileDescriptor |
module |
no |
Suggested fix
PrintEnumValues in the same file already models this distinction:
void PyiGenerator::PrintEnumValues(const EnumDescriptor& enum_descriptor,
bool is_classvar) const {
...
if (is_classvar) {
printer_->Print("$name$: _ClassVar[$module_enum_name$]\n", ...);
} else {
printer_->Print("$name$: $module_enum_name$\n", ...);
Giving PrintExtensions the same bool is_classvar = false parameter and passing true only from the message call site would match that idiom without needing a template specialisation.
Happy to send a PR.
What version of protobuf and what language are you using?
Version:
libprotoc 33.5(also present onmain)Language: Python (
--pyi_out)What did you do?
Compile a proto that declares a file-level extension:
What did you expect to see?
A
.pyithat type-checks.What did you see instead?
ClassVaris emitted at module scope, where it is not a valid annotation:Only the module-scope one is flagged; the nested one is fine. PEP 526 restricts
ClassVarto class bodies.Cause
PyiGenerator::PrintExtensions(src/google/protobuf/compiler/python/pyi_generator.cc:364) is a template overDescriptorTand emits_ClassVar[int]unconditionally:It is instantiated for both scopes:
_ClassVarvalid:481PrintExtensions(message_descriptor)Descriptor:656PrintExtensions(*public_dep)FileDescriptor:669PrintExtensions(*file_)FileDescriptorSuggested fix
PrintEnumValuesin the same file already models this distinction:Giving
PrintExtensionsthe samebool is_classvar = falseparameter and passingtrueonly from the message call site would match that idiom without needing a template specialisation.Happy to send a PR.