Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## Upcoming

- Require protobuf 3.18.1
- Change `EnumTypeWrapper.V` to `EnumTypeWrapper.ValueType` per https://github.com/protocolbuffers/protobuf/pull/8182.
Will allow for unquoted annotations starting with protobuf 3.20.0. `.V` will continue to work for the foreseeable
future for backward compatibility.

## 3.0.0

Expand Down
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,32 +87,33 @@ enum MyEnum {
BAR = 1;
}
```
Will yield an [enum type wrapper](https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stubs/protobuf/google/protobuf/internal/enum_type_wrapper.pyi) whose methods type to `MyEnum.V` rather than `int`.
Will yield an [enum type wrapper](https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stubs/protobuf/google/protobuf/internal/enum_type_wrapper.pyi) whose methods type to `MyEnum.ValueType` (a `NewType(int)` rather than `int`.
This allows mypy to catch bugs where the wrong enum value is being used.

Calling code may be typed as follows.

In python >= 3.7
```python
# Need [PEP 563](https://www.python.org/dev/peps/pep-0563/) to postpone evaluation of annotations
from __future__ import annotations # Not needed with python>=3.10
def f(x: MyEnum.V):
# May need [PEP 563](https://www.python.org/dev/peps/pep-0563/) to postpone evaluation of annotations
# from __future__ import annotations # Not needed with python>=3.10 or protobuf>=3.20.0
def f(x: MyEnum.ValueType):
print(x)
f(MyEnum.Value("FOO"))
```

For usages of cast, the type of `x` must be quoted
until [upstream protobuf](https://github.com/protocolbuffers/protobuf/pull/8182) includes `V`
With protobuf <= 3.20.0, for usages of cast, the type of `x` must be quoted
After protobuf >= 3.20.0 - `ValueType` exists in the python code and quotes aren't needed
until [upstream protobuf](https://github.com/protocolbuffers/protobuf/pull/8182) includes `ValueType`
```python
cast('MyEnum.V', x)
cast('MyEnum.ValueType', x)
```

Similarly, for type aliases, you must either quote the type or hide it behind `TYPE_CHECKING`
Similarly, for type aliases with protobuf < 3.20.0, you must either quote the type or hide it behind `TYPE_CHECKING`
```python
from typing import Tuple, TYPE_CHECKING
FOO = Tuple['MyEnum.V', 'MyEnum.V']
FOO = Tuple['MyEnum.ValueType', 'MyEnum.ValueType']
if TYPE_CHECKING:
FOO = Tuple[MyEnum.V, MyEnum.V]
FOO = Tuple[MyEnum.ValueType, MyEnum.ValueType]
```

#### Enum int impl details
Expand All @@ -123,18 +124,20 @@ mypy-protobuf autogenerates an instance of the EnumTypeWrapper as follows.
class MyEnum(_MyEnum, metaclass=_MyEnumEnumTypeWrapper):
pass
class _MyEnum:
V = typing.NewType('V', builtins.int)
class _MyEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MyEnum.V], builtins.type):
ValueType = typing.NewType('ValueType', builtins.int)
V = Union[ValueType]
class _MyEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MyEnum.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ...
FOO = MyEnum.V(0)
BAR = MyEnum.V(1)
FOO = MyEnum.V(0)
BAR = MyEnum.V(1)
FOO = MyEnum.ValueType(0)
BAR = MyEnum.ValueType(1)
FOO = MyEnum.ValueType(0)
BAR = MyEnum.ValueType(1)
```

`_MyEnumEnumTypeWrapper` extends the EnumTypeWrapper to take/return MyEnum.V rather than int
`_MyEnumEnumTypeWrapper` extends the EnumTypeWrapper to take/return MyEnum.ValueType rather than int
`MyEnum` is an instance of the `EnumTypeWrapper`.
- Use `_MyEnum` and of metaclass is an implementation detail to make MyEnum.V a valid type w/o a circular dependency
- Use `_MyEnum` and of metaclass is an implementation detail to make MyEnum.ValueType a valid type w/o a circular dependency
- `V` is supported as an alias of `ValueType` for backward compatibility



Expand Down
14 changes: 9 additions & 5 deletions mypy_protobuf/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _add_enums(
) -> None:
for enum in enums:
self.message_to_fd[prefix + enum.name] = _fd
self.message_to_fd[prefix + enum.name + ".V"] = _fd
self.message_to_fd[prefix + enum.name + ".ValueType"] = _fd

def _add_messages(
messages: "RepeatedCompositeFieldContainer[d.DescriptorProto]",
Expand Down Expand Up @@ -332,7 +332,7 @@ def write_enums(
class_name = (
enum.name if enum.name not in PYTHON_RESERVED else "_r_" + enum.name
)
value_type_fq = prefix + class_name + ".V"
value_type_fq = prefix + class_name + ".ValueType"

l(
"class {}({}, metaclass={}):",
Expand All @@ -347,17 +347,21 @@ def write_enums(
l("class {}:", "_" + enum.name)
with self._indent():
l(
"V = {}('V', {})",
"ValueType = {}('ValueType', {})",
self._import("typing", "NewType"),
self._builtin("int"),
)
# Ideally this would be `V: TypeAlias = ValueType`, but it appears
# to be buggy in mypy in nested scopes
# Workaround described here https://github.com/python/mypy/issues/7866
l("V = {}[ValueType]", self._import("typing", "Union"))
l(
"class {}({}[{}], {}):",
"_" + enum.name + "EnumTypeWrapper",
self._import(
"google.protobuf.internal.enum_type_wrapper", "_EnumTypeWrapper"
),
"_" + enum.name + ".V",
"_" + enum.name + ".ValueType",
self._builtin("type"),
)
with self._indent():
Expand Down Expand Up @@ -864,7 +868,7 @@ def python_type(
d.FieldDescriptorProto.TYPE_STRING: lambda: self._import("typing", "Text"),
d.FieldDescriptorProto.TYPE_BYTES: lambda: self._builtin("bytes"),
d.FieldDescriptorProto.TYPE_ENUM: lambda: self._import_message(
field.type_name + ".V"
field.type_name + ".ValueType"
),
d.FieldDescriptorProto.TYPE_MESSAGE: lambda: self._import_message(
field.type_name
Expand Down
4 changes: 2 additions & 2 deletions test/generated/testproto/inner/inner_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ...
class Inner(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor = ...
A_FIELD_NUMBER: builtins.int
a: testproto.test3_pb2.OuterEnum.V = ...
a: testproto.test3_pb2.OuterEnum.ValueType = ...
def __init__(self,
*,
a : testproto.test3_pb2.OuterEnum.V = ...,
a : testproto.test3_pb2.OuterEnum.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["a",b"a"]) -> None: ...
global___Inner = Inner
46 changes: 24 additions & 22 deletions test/generated/testproto/nested/nested_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ...
class Nested(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor = ...
A_FIELD_NUMBER: builtins.int
a: testproto.test3_pb2.OuterEnum.V = ...
a: testproto.test3_pb2.OuterEnum.ValueType = ...
def __init__(self,
*,
a : testproto.test3_pb2.OuterEnum.V = ...,
a : testproto.test3_pb2.OuterEnum.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["a",b"a"]) -> None: ...
global___Nested = Nested
Expand All @@ -28,47 +28,49 @@ class AnotherNested(google.protobuf.message.Message):
class NestedEnum(_NestedEnum, metaclass=_NestedEnumEnumTypeWrapper):
pass
class _NestedEnum:
V = typing.NewType('V', builtins.int)
class _NestedEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_NestedEnum.V], builtins.type):
ValueType = typing.NewType('ValueType', builtins.int)
V = typing.Union[ValueType]
class _NestedEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_NestedEnum.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ...
INVALID = AnotherNested.NestedEnum.V(0)
ONE = AnotherNested.NestedEnum.V(1)
TWO = AnotherNested.NestedEnum.V(2)
INVALID = AnotherNested.NestedEnum.ValueType(0)
ONE = AnotherNested.NestedEnum.ValueType(1)
TWO = AnotherNested.NestedEnum.ValueType(2)

INVALID = AnotherNested.NestedEnum.V(0)
ONE = AnotherNested.NestedEnum.V(1)
TWO = AnotherNested.NestedEnum.V(2)
INVALID = AnotherNested.NestedEnum.ValueType(0)
ONE = AnotherNested.NestedEnum.ValueType(1)
TWO = AnotherNested.NestedEnum.ValueType(2)

class NestedMessage(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor = ...
class NestedEnum2(_NestedEnum2, metaclass=_NestedEnum2EnumTypeWrapper):
pass
class _NestedEnum2:
V = typing.NewType('V', builtins.int)
class _NestedEnum2EnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_NestedEnum2.V], builtins.type):
ValueType = typing.NewType('ValueType', builtins.int)
V = typing.Union[ValueType]
class _NestedEnum2EnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_NestedEnum2.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ...
UNDEFINED = AnotherNested.NestedMessage.NestedEnum2.V(0)
NESTED_ENUM1 = AnotherNested.NestedMessage.NestedEnum2.V(1)
NESTED_ENUM2 = AnotherNested.NestedMessage.NestedEnum2.V(2)
UNDEFINED = AnotherNested.NestedMessage.NestedEnum2.ValueType(0)
NESTED_ENUM1 = AnotherNested.NestedMessage.NestedEnum2.ValueType(1)
NESTED_ENUM2 = AnotherNested.NestedMessage.NestedEnum2.ValueType(2)

UNDEFINED = AnotherNested.NestedMessage.NestedEnum2.V(0)
NESTED_ENUM1 = AnotherNested.NestedMessage.NestedEnum2.V(1)
NESTED_ENUM2 = AnotherNested.NestedMessage.NestedEnum2.V(2)
UNDEFINED = AnotherNested.NestedMessage.NestedEnum2.ValueType(0)
NESTED_ENUM1 = AnotherNested.NestedMessage.NestedEnum2.ValueType(1)
NESTED_ENUM2 = AnotherNested.NestedMessage.NestedEnum2.ValueType(2)

S_FIELD_NUMBER: builtins.int
B_FIELD_NUMBER: builtins.int
NE_FIELD_NUMBER: builtins.int
NE2_FIELD_NUMBER: builtins.int
s: typing.Text = ...
b: builtins.bool = ...
ne: global___AnotherNested.NestedEnum.V = ...
ne2: global___AnotherNested.NestedMessage.NestedEnum2.V = ...
ne: global___AnotherNested.NestedEnum.ValueType = ...
ne2: global___AnotherNested.NestedMessage.NestedEnum2.ValueType = ...
def __init__(self,
*,
s : typing.Text = ...,
b : builtins.bool = ...,
ne : global___AnotherNested.NestedEnum.V = ...,
ne2 : global___AnotherNested.NestedMessage.NestedEnum2.V = ...,
ne : global___AnotherNested.NestedEnum.ValueType = ...,
ne2 : global___AnotherNested.NestedMessage.NestedEnum2.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["b",b"b","ne",b"ne","ne2",b"ne2","s",b"s"]) -> None: ...

Expand Down
50 changes: 26 additions & 24 deletions test/generated/testproto/test3_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ...
class OuterEnum(_OuterEnum, metaclass=_OuterEnumEnumTypeWrapper):
pass
class _OuterEnum:
V = typing.NewType('V', builtins.int)
class _OuterEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_OuterEnum.V], builtins.type):
ValueType = typing.NewType('ValueType', builtins.int)
V = typing.Union[ValueType]
class _OuterEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_OuterEnum.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ...
UNKNOWN = OuterEnum.V(0)
FOO3 = OuterEnum.V(1)
BAR3 = OuterEnum.V(2)
UNKNOWN = OuterEnum.ValueType(0)
FOO3 = OuterEnum.ValueType(1)
BAR3 = OuterEnum.ValueType(2)

UNKNOWN = OuterEnum.V(0)
FOO3 = OuterEnum.V(1)
BAR3 = OuterEnum.V(2)
UNKNOWN = OuterEnum.ValueType(0)
FOO3 = OuterEnum.ValueType(1)
BAR3 = OuterEnum.ValueType(2)
global___OuterEnum = OuterEnum


Expand All @@ -44,14 +45,15 @@ class SimpleProto3(google.protobuf.message.Message):
class InnerEnum(_InnerEnum, metaclass=_InnerEnumEnumTypeWrapper):
pass
class _InnerEnum:
V = typing.NewType('V', builtins.int)
class _InnerEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_InnerEnum.V], builtins.type):
ValueType = typing.NewType('ValueType', builtins.int)
V = typing.Union[ValueType]
class _InnerEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_InnerEnum.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ...
INNER1 = SimpleProto3.InnerEnum.V(0)
INNER2 = SimpleProto3.InnerEnum.V(1)
INNER1 = SimpleProto3.InnerEnum.ValueType(0)
INNER2 = SimpleProto3.InnerEnum.ValueType(1)

INNER1 = SimpleProto3.InnerEnum.V(0)
INNER2 = SimpleProto3.InnerEnum.V(1)
INNER1 = SimpleProto3.InnerEnum.ValueType(0)
INNER2 = SimpleProto3.InnerEnum.ValueType(1)

class MapScalarEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor = ...
Expand Down Expand Up @@ -102,21 +104,21 @@ class SimpleProto3(google.protobuf.message.Message):
a_string: typing.Text = ...
@property
def a_repeated_string(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[typing.Text]: ...
a_outer_enum: global___OuterEnum.V = ...
a_outer_enum: global___OuterEnum.ValueType = ...
@property
def outer_message(self) -> global___OuterMessage3: ...
inner_enum: global___SimpleProto3.InnerEnum.V = ...
inner_enum: global___SimpleProto3.InnerEnum.ValueType = ...
a_oneof_1: typing.Text = ...
a_oneof_2: typing.Text = ...
@property
def outer_message_in_oneof(self) -> global___OuterMessage3: ...
outer_enum_in_oneof: global___OuterEnum.V = ...
inner_enum_in_oneof: global___SimpleProto3.InnerEnum.V = ...
outer_enum_in_oneof: global___OuterEnum.ValueType = ...
inner_enum_in_oneof: global___SimpleProto3.InnerEnum.ValueType = ...
b_oneof_1: typing.Text = ...
b_oneof_2: typing.Text = ...
@property
def bool(self) -> global___OuterMessage3: ...
OuterEnum: global___OuterEnum.V = ...
OuterEnum: global___OuterEnum.ValueType = ...
"""Test having fieldname match messagename"""

@property
Expand All @@ -132,18 +134,18 @@ class SimpleProto3(google.protobuf.message.Message):
*,
a_string : typing.Text = ...,
a_repeated_string : typing.Optional[typing.Iterable[typing.Text]] = ...,
a_outer_enum : global___OuterEnum.V = ...,
a_outer_enum : global___OuterEnum.ValueType = ...,
outer_message : typing.Optional[global___OuterMessage3] = ...,
inner_enum : global___SimpleProto3.InnerEnum.V = ...,
inner_enum : global___SimpleProto3.InnerEnum.ValueType = ...,
a_oneof_1 : typing.Text = ...,
a_oneof_2 : typing.Text = ...,
outer_message_in_oneof : typing.Optional[global___OuterMessage3] = ...,
outer_enum_in_oneof : global___OuterEnum.V = ...,
inner_enum_in_oneof : global___SimpleProto3.InnerEnum.V = ...,
outer_enum_in_oneof : global___OuterEnum.ValueType = ...,
inner_enum_in_oneof : global___SimpleProto3.InnerEnum.ValueType = ...,
b_oneof_1 : typing.Text = ...,
b_oneof_2 : typing.Text = ...,
bool : typing.Optional[global___OuterMessage3] = ...,
OuterEnum : global___OuterEnum.V = ...,
OuterEnum : global___OuterEnum.ValueType = ...,
OuterMessage3 : typing.Optional[global___OuterMessage3] = ...,
map_scalar : typing.Optional[typing.Mapping[builtins.int, typing.Text]] = ...,
map_message : typing.Optional[typing.Mapping[builtins.int, global___OuterMessage3]] = ...,
Expand Down
4 changes: 2 additions & 2 deletions test/generated/testproto/test_extensions3_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ scalar_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor

repeated_scalar_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, google.protobuf.internal.containers.RepeatedScalarFieldContainer[typing.Text]] = ...

enum_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, testproto.test3_pb2.OuterEnum.V] = ...
enum_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, testproto.test3_pb2.OuterEnum.ValueType] = ...

repeated_enum_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, google.protobuf.internal.containers.RepeatedScalarFieldContainer[testproto.test3_pb2.OuterEnum.V]] = ...
repeated_enum_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, google.protobuf.internal.containers.RepeatedScalarFieldContainer[testproto.test3_pb2.OuterEnum.ValueType]] = ...

msg_option: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.MessageOptions, testproto.test3_pb2.OuterMessage3] = ...

Expand Down
Loading