Skip to content

Commit 1989920

Browse files
authored
Use a single extension message for all the mypy_protobuf field extension types (#398)
This allows us to only expend a single extension number
1 parent 7d741e6 commit 1989920

File tree

13 files changed

+171
-23
lines changed

13 files changed

+171
-23
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Upcoming
22

3+
- Prefer (mypy_protobuf.options).casttype to (mypy_protobuf.casttype)
4+
- Allows us to use a single extension number
5+
- Applies to casttype,keytype,valuetype
6+
- Deprecate (but still support) the old-style extension
37
- Support emitting module docstrings
48
- Make generated code flake8 compatible
59
- Convert extensions.proto to proto3

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ WORLD: MyEnum.ValueType # 1
151151
M.proto
152152
```proto
153153
message M {
154-
uint32 user_id = 1 [(mypy_protobuf.casttype)="mymod.UserId"];
154+
uint32 user_id = 1 [(mypy_protobuf.options).casttype="mymod.UserId"];
155155
map<uint32, string> email_by_uid = 2 [
156-
(mypy_protobuf.keytype)="path/to/mymod.UserId",
157-
(mypy_protobuf.valuetype)="path/to/mymod.Email"
156+
(mypy_protobuf.options).keytype="path/to/mymod.UserId",
157+
(mypy_protobuf.options).valuetype="path/to/mymod.Email"
158158
];
159159
}
160160
```

mypy_protobuf/extensions_pb2.py

+20-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mypy_protobuf/extensions_pb2.pyi

+29-3
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,41 @@ import builtins
66
import google.protobuf.descriptor
77
import google.protobuf.descriptor_pb2
88
import google.protobuf.internal.extension_dict
9+
import google.protobuf.message
10+
import typing_extensions
911

1012
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
1113

14+
class FieldOptions(google.protobuf.message.Message):
15+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
16+
17+
CASTTYPE_FIELD_NUMBER: builtins.int
18+
KEYTYPE_FIELD_NUMBER: builtins.int
19+
VALUETYPE_FIELD_NUMBER: builtins.int
20+
casttype: builtins.str
21+
"""Tells mypy-protobuf to use a specific newtype rather than the normal type for this field."""
22+
keytype: builtins.str
23+
"""Tells mypy-protobuf to use a specific type for keys; only makes sense on map fields"""
24+
valuetype: builtins.str
25+
"""Tells mypy-protobuf to use a specific type for values; only makes sense on map fields"""
26+
def __init__(
27+
self,
28+
*,
29+
casttype: builtins.str = ...,
30+
keytype: builtins.str = ...,
31+
valuetype: builtins.str = ...,
32+
) -> None: ...
33+
def ClearField(self, field_name: typing_extensions.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]) -> None: ...
34+
35+
global___FieldOptions = FieldOptions
36+
37+
OPTIONS_FIELD_NUMBER: builtins.int
1238
CASTTYPE_FIELD_NUMBER: builtins.int
1339
KEYTYPE_FIELD_NUMBER: builtins.int
1440
VALUETYPE_FIELD_NUMBER: builtins.int
41+
options: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, global___FieldOptions]
42+
"""Custom field options from mypy-protobuf"""
1543
casttype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
16-
"""Tells mypy to use a specific newtype rather than the normal type for this field."""
44+
"""Legacy fields. Prefer to use ones within `options` instead."""
1745
keytype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
18-
"""Tells mypy to use a specific type for keys; only makes sense on map fields"""
1946
valuetype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
20-
"""Tells mypy to use a specific type for values; only makes sense on map fields"""

mypy_protobuf/main.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -634,10 +634,18 @@ def _map_key_value_types(
634634
key_field: d.FieldDescriptorProto,
635635
value_field: d.FieldDescriptorProto,
636636
) -> Tuple[str, str]:
637-
key_casttype = map_field.options.Extensions[extensions_pb2.keytype]
637+
oldstyle_keytype = map_field.options.Extensions[extensions_pb2.keytype]
638+
if oldstyle_keytype:
639+
print(f"Warning: Map Field {map_field.name}: (mypy_protobuf.keytype) is deprecated. Prefer (mypy_protobuf.options).keytype", file=sys.stderr)
640+
key_casttype = map_field.options.Extensions[extensions_pb2.options].keytype or oldstyle_keytype
638641
ktype = self._import_casttype(key_casttype) if key_casttype else self.python_type(key_field)
639-
value_casttype = map_field.options.Extensions[extensions_pb2.valuetype]
642+
643+
oldstyle_valuetype = map_field.options.Extensions[extensions_pb2.valuetype]
644+
if oldstyle_valuetype:
645+
print(f"Warning: Map Field {map_field.name}: (mypy_protobuf.valuetype) is deprecated. Prefer (mypy_protobuf.options).valuetype", file=sys.stderr)
646+
value_casttype = map_field.options.Extensions[extensions_pb2.options].valuetype or map_field.options.Extensions[extensions_pb2.valuetype]
640647
vtype = self._import_casttype(value_casttype) if value_casttype else self.python_type(value_field)
648+
641649
return ktype, vtype
642650

643651
def _callable_type(self, method: d.MethodDescriptorProto) -> str:
@@ -758,7 +766,10 @@ def python_type(self, field: d.FieldDescriptorProto, generic_container: bool = F
758766
- Mapping[k, v] rather than MessageMap[k, v]
759767
Can be useful for input types (eg constructor)
760768
"""
761-
casttype = field.options.Extensions[extensions_pb2.casttype]
769+
oldstyle_casttype = field.options.Extensions[extensions_pb2.casttype]
770+
if oldstyle_casttype:
771+
print(f"Warning: Field {field.name}: (mypy_protobuf.casttype) is deprecated. Prefer (mypy_protobuf.options).casttype", file=sys.stderr)
772+
casttype = field.options.Extensions[extensions_pb2.options].casttype or oldstyle_casttype
762773
if casttype:
763774
return self._import_casttype(casttype)
764775

proto/mypy_protobuf/extensions.proto

+16-6
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@ package mypy_protobuf;
44

55
import "google/protobuf/descriptor.proto";
66

7+
message FieldOptions {
8+
// Tells mypy-protobuf to use a specific newtype rather than the normal type for this field.
9+
string casttype = 1;
10+
// Tells mypy-protobuf to use a specific type for keys; only makes sense on map fields
11+
string keytype = 2;
12+
// Tells mypy-protobuf to use a specific type for values; only makes sense on map fields
13+
string valuetype = 3;
14+
15+
}
16+
717
extend google.protobuf.FieldOptions {
8-
// Tells mypy to use a specific newtype rather than the normal type for this field.
9-
string casttype = 60000;
18+
// Custom field options from mypy-protobuf
19+
FieldOptions options = 60004;
1020

11-
// Tells mypy to use a specific type for keys; only makes sense on map fields
12-
string keytype = 60002;
13-
// Tells mypy to use a specific type for values; only makes sense on map fields
14-
string valuetype = 60003;
21+
// Legacy fields. Prefer to use ones within `options` instead.
22+
string casttype = 60000 [deprecated = true];
23+
string keytype = 60002 [deprecated = true];
24+
string valuetype = 60003 [deprecated = true];
1525
}

proto/testproto/test3.proto

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ syntax = "proto3";
88
/* package test3 */
99
package test3;
1010

11+
import "mypy_protobuf/extensions.proto";
12+
1113
enum OuterEnum {
1214
UNKNOWN = 0;
1315
FOO3 = 1;
@@ -54,4 +56,11 @@ message SimpleProto3 {
5456
map<int32, OuterMessage3> map_message = 17;
5557

5658
optional string an_optional_string = 18;
59+
60+
uint32 user_id = 19 [(mypy_protobuf.options).casttype="test/test_generated_mypy.UserId"];
61+
string email = 20 [(mypy_protobuf.options).casttype="test/test_generated_mypy.Email"];
62+
map<uint32, string> email_by_uid = 21 [
63+
(mypy_protobuf.options).keytype="test/test_generated_mypy.UserId",
64+
(mypy_protobuf.options).valuetype="test/test_generated_mypy.Email"
65+
];
5766
}

run_test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
170170
echo -e "${RED}test_negative/output.expected.$PY_VER_MYPY_TARGET didnt match. Copying over for you. Now rerun${NC}"
171171

172172
# Copy over all the mypy results for the developer.
173-
call_mypy 3.8.11 "${NEGATIVE_FILES[@]}"
173+
call_mypy $PY_VER "${NEGATIVE_FILES[@]}"
174174
cp "$MYPY_OUTPUT/mypy_output" test_negative/output.expected.3.8
175175
cp "$MYPY_OUTPUT/mypy_output.omit_linenos" test_negative/output.expected.3.8.omit_linenos
176176
exit 1

test/generated/mypy_protobuf/extensions_pb2.pyi

+29-3
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,41 @@ import builtins
66
import google.protobuf.descriptor
77
import google.protobuf.descriptor_pb2
88
import google.protobuf.internal.extension_dict
9+
import google.protobuf.message
10+
import typing_extensions
911

1012
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
1113

14+
class FieldOptions(google.protobuf.message.Message):
15+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
16+
17+
CASTTYPE_FIELD_NUMBER: builtins.int
18+
KEYTYPE_FIELD_NUMBER: builtins.int
19+
VALUETYPE_FIELD_NUMBER: builtins.int
20+
casttype: builtins.str
21+
"""Tells mypy-protobuf to use a specific newtype rather than the normal type for this field."""
22+
keytype: builtins.str
23+
"""Tells mypy-protobuf to use a specific type for keys; only makes sense on map fields"""
24+
valuetype: builtins.str
25+
"""Tells mypy-protobuf to use a specific type for values; only makes sense on map fields"""
26+
def __init__(
27+
self,
28+
*,
29+
casttype: builtins.str = ...,
30+
keytype: builtins.str = ...,
31+
valuetype: builtins.str = ...,
32+
) -> None: ...
33+
def ClearField(self, field_name: typing_extensions.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]) -> None: ...
34+
35+
global___FieldOptions = FieldOptions
36+
37+
OPTIONS_FIELD_NUMBER: builtins.int
1238
CASTTYPE_FIELD_NUMBER: builtins.int
1339
KEYTYPE_FIELD_NUMBER: builtins.int
1440
VALUETYPE_FIELD_NUMBER: builtins.int
41+
options: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, global___FieldOptions]
42+
"""Custom field options from mypy-protobuf"""
1543
casttype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
16-
"""Tells mypy to use a specific newtype rather than the normal type for this field."""
44+
"""Legacy fields. Prefer to use ones within `options` instead."""
1745
keytype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
18-
"""Tells mypy to use a specific type for keys; only makes sense on map fields"""
1946
valuetype: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[google.protobuf.descriptor_pb2.FieldOptions, builtins.str]
20-
"""Tells mypy to use a specific type for values; only makes sense on map fields"""

test/generated/testproto/test3_pb2.pyi

+27-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import google.protobuf.descriptor
88
import google.protobuf.internal.containers
99
import google.protobuf.internal.enum_type_wrapper
1010
import google.protobuf.message
11+
import test.test_generated_mypy
1112
import typing
1213
import typing_extensions
1314

@@ -92,6 +93,21 @@ class SimpleProto3(google.protobuf.message.Message):
9293
def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ...
9394
def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ...
9495

96+
class EmailByUidEntry(google.protobuf.message.Message):
97+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
98+
99+
KEY_FIELD_NUMBER: builtins.int
100+
VALUE_FIELD_NUMBER: builtins.int
101+
key: builtins.int
102+
value: builtins.str
103+
def __init__(
104+
self,
105+
*,
106+
key: builtins.int = ...,
107+
value: builtins.str = ...,
108+
) -> None: ...
109+
def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ...
110+
95111
A_STRING_FIELD_NUMBER: builtins.int
96112
A_REPEATED_STRING_FIELD_NUMBER: builtins.int
97113
A_OUTER_ENUM_FIELD_NUMBER: builtins.int
@@ -110,6 +126,9 @@ class SimpleProto3(google.protobuf.message.Message):
110126
MAP_SCALAR_FIELD_NUMBER: builtins.int
111127
MAP_MESSAGE_FIELD_NUMBER: builtins.int
112128
AN_OPTIONAL_STRING_FIELD_NUMBER: builtins.int
129+
USER_ID_FIELD_NUMBER: builtins.int
130+
EMAIL_FIELD_NUMBER: builtins.int
131+
EMAIL_BY_UID_FIELD_NUMBER: builtins.int
113132
a_string: builtins.str
114133
@property
115134
def a_repeated_string(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
@@ -137,6 +156,10 @@ class SimpleProto3(google.protobuf.message.Message):
137156
@property
138157
def map_message(self) -> google.protobuf.internal.containers.MessageMap[builtins.int, global___OuterMessage3]: ...
139158
an_optional_string: builtins.str
159+
user_id: test.test_generated_mypy.UserId
160+
email: test.test_generated_mypy.Email
161+
@property
162+
def email_by_uid(self) -> google.protobuf.internal.containers.ScalarMap[test.test_generated_mypy.UserId, test.test_generated_mypy.Email]: ...
140163
def __init__(
141164
self,
142165
*,
@@ -158,9 +181,12 @@ class SimpleProto3(google.protobuf.message.Message):
158181
map_scalar: collections.abc.Mapping[builtins.int, builtins.str] | None = ...,
159182
map_message: collections.abc.Mapping[builtins.int, global___OuterMessage3] | None = ...,
160183
an_optional_string: builtins.str | None = ...,
184+
user_id: test.test_generated_mypy.UserId = ...,
185+
email: test.test_generated_mypy.Email = ...,
186+
email_by_uid: collections.abc.Mapping[test.test_generated_mypy.UserId, test.test_generated_mypy.Email] | None = ...,
161187
) -> None: ...
162188
def HasField(self, field_name: typing_extensions.Literal["OuterMessage3", b"OuterMessage3", "_an_optional_string", b"_an_optional_string", "a_oneof", b"a_oneof", "a_oneof_1", b"a_oneof_1", "a_oneof_2", b"a_oneof_2", "an_optional_string", b"an_optional_string", "b_oneof", b"b_oneof", "b_oneof_1", b"b_oneof_1", "b_oneof_2", b"b_oneof_2", "bool", b"bool", "inner_enum_in_oneof", b"inner_enum_in_oneof", "outer_enum_in_oneof", b"outer_enum_in_oneof", "outer_message", b"outer_message", "outer_message_in_oneof", b"outer_message_in_oneof"]) -> builtins.bool: ...
163-
def ClearField(self, field_name: typing_extensions.Literal["OuterEnum", b"OuterEnum", "OuterMessage3", b"OuterMessage3", "_an_optional_string", b"_an_optional_string", "a_oneof", b"a_oneof", "a_oneof_1", b"a_oneof_1", "a_oneof_2", b"a_oneof_2", "a_outer_enum", b"a_outer_enum", "a_repeated_string", b"a_repeated_string", "a_string", b"a_string", "an_optional_string", b"an_optional_string", "b_oneof", b"b_oneof", "b_oneof_1", b"b_oneof_1", "b_oneof_2", b"b_oneof_2", "bool", b"bool", "inner_enum", b"inner_enum", "inner_enum_in_oneof", b"inner_enum_in_oneof", "map_message", b"map_message", "map_scalar", b"map_scalar", "outer_enum_in_oneof", b"outer_enum_in_oneof", "outer_message", b"outer_message", "outer_message_in_oneof", b"outer_message_in_oneof"]) -> None: ...
189+
def ClearField(self, field_name: typing_extensions.Literal["OuterEnum", b"OuterEnum", "OuterMessage3", b"OuterMessage3", "_an_optional_string", b"_an_optional_string", "a_oneof", b"a_oneof", "a_oneof_1", b"a_oneof_1", "a_oneof_2", b"a_oneof_2", "a_outer_enum", b"a_outer_enum", "a_repeated_string", b"a_repeated_string", "a_string", b"a_string", "an_optional_string", b"an_optional_string", "b_oneof", b"b_oneof", "b_oneof_1", b"b_oneof_1", "b_oneof_2", b"b_oneof_2", "bool", b"bool", "email", b"email", "email_by_uid", b"email_by_uid", "inner_enum", b"inner_enum", "inner_enum_in_oneof", b"inner_enum_in_oneof", "map_message", b"map_message", "map_scalar", b"map_scalar", "outer_enum_in_oneof", b"outer_enum_in_oneof", "outer_message", b"outer_message", "outer_message_in_oneof", b"outer_message_in_oneof", "user_id", b"user_id"]) -> None: ...
164190
@typing.overload
165191
def WhichOneof(self, oneof_group: typing_extensions.Literal["_an_optional_string", b"_an_optional_string"]) -> typing_extensions.Literal["an_optional_string"] | None: ...
166192
@typing.overload

test/test_generated_mypy.py

+17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import testproto.test_pb2 as test_pb2
1717
from google.protobuf.descriptor import FieldDescriptor
1818
from google.protobuf.internal import api_implementation
19+
from google.protobuf.internal.containers import ScalarMap
1920
from google.protobuf.message import Message
2021
from testproto.Capitalized.Capitalized_pb2 import Upper, lower, lower2
2122
from testproto.reexport_pb2 import FOO3 as ReexportedFOO3
@@ -41,6 +42,7 @@
4142
Simple1,
4243
Simple2,
4344
)
45+
from typing_extensions import assert_type
4446

4547
# C++ python API implementation has some semantic differences from pure python
4648
# We mainly focus on testing the C++ impl - but just have some flags here
@@ -474,12 +476,27 @@ def test_mapping_type() -> None:
474476

475477
def test_casttype() -> None:
476478
s = Simple1()
479+
assert_type(s.user_id, UserId)
480+
assert_type(s.email, Email)
481+
assert_type(s.email_by_uid, ScalarMap[UserId, Email])
482+
477483
s.user_id = UserId(33)
478484
assert s.user_id == 33
479485
s.email = Email("[email protected]")
480486
assert s.email == "[email protected]"
481487
s.email_by_uid[UserId(33)] = Email("[email protected]")
482488

489+
s2 = SimpleProto3()
490+
assert_type(s2.user_id, UserId)
491+
assert_type(s2.email, Email)
492+
assert_type(s2.email_by_uid, ScalarMap[UserId, Email])
493+
494+
s2.user_id = UserId(33)
495+
assert s2.user_id == 33
496+
s2.email = Email("[email protected]")
497+
assert s2.email == "[email protected]"
498+
s2.email_by_uid[UserId(33)] = Email("[email protected]")
499+
483500

484501
def test_reserved_keywords() -> None:
485502
with pytest.raises(AttributeError, match="module.*has no attribute 'asdf'"):

0 commit comments

Comments
 (0)