Skip to content

Python: --pyi_out emits ClassVar[int] at module scope for file-level extensions #29221

Description

@folded

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonuntriagedauto added to all issues by default when created.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions