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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ __pycache__/
*.pyc
/test/generated/**/*.py
!/test/generated/**/__init__.py
/test/generated-concrete/**/*.py
!/test/generated-concrete/**/__init__.py
/test/generated_concrete/**/*.py
!/test/generated_concrete/**/__init__.py
.pytest_cache
/build/
/dist/
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- Protobuf <6.32 still had the edition enums and field options, so it *should* still work. But is untested
- Add support for editions (up to 2024)
- Add `generate_concrete_servicer_stubs` option to generate concrete instead of abstract servicer stubs
- Add `_HasFieldArgType` and `_ClearFieldArgType` aliases to allow for typing field manipulation functions
- Add `_WhichOneofArgType_<oneof_name>` and `_WhichOneofReturnType_<oneof_name>` type aliases

## 3.7.0

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ See [Changelog](CHANGELOG.md) for full listing
* `mypy-protobuf` generates correctly typed constructors dependinding on field presence.
* `mypy-protobuf` generates correctly typed `HasField`, `WhichOneof`, and `ClearField` methods.
* There are differences in how `mypy-protobuf` and `pyi_out` generate enums. See [this issue](https://github.com/protocolbuffers/protobuf/issues/8175) for details
* Type aliases exported for `HasField`, `WhichOneof` and `ClearField` arguments

#### Examples

Expand Down Expand Up @@ -370,6 +371,29 @@ protoc \
Note that generated code for grpc will work only together with code for python and locations should be the same.
If you need stubs for grpc internal code we suggest using this package https://github.com/shabbyrobe/grpc-stubs

### `_ClearFieldArgType`, `_WhichOneofArgType_<oneof_name>`, `_WhichOneofReturnType_<oneof_name>` and `_HasFieldArgType` aliases

Where applicable, type aliases are generated for the arguments to `ClearField`, `WhichOneof` and `HasField`. These can be used to create typed functions for field manipulation:

```python
from testproto.edition2024_pb2 import Editions2024Test

def test_hasfield_alias(msg: Editions2024Test, field: "Editions2024Test._HasFieldArgType") -> bool:
return msg.HasField(field)

test_hasfield_alias(Editions2024Test(), "legacy")

def test_whichoneof_alias(
msg: SimpleProto3,
oneof: "SimpleProto3._WhichOneofArgType_a_oneof",
) -> "SimpleProto3._WhichOneofReturnType_a_oneof | None":
return msg.WhichOneof(oneof)

test_whichoneof_alias(SimpleProto3(), "a_oneof")
```

Note the deferred evaluation (string reference, or `from __future__ import annotations`. This bypasses the fact that the alias does not exist on the runtime class)

### Targeting python2 support

mypy-protobuf's drops support for targeting python2 with version 3.0. If you still need python2 support -
Expand Down
3 changes: 2 additions & 1 deletion mypy_protobuf/extensions_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class FieldOptions(google.protobuf.message.Message):
keytype: builtins.str = ...,
valuetype: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___FieldOptions: typing_extensions.TypeAlias = FieldOptions

Expand Down
36 changes: 24 additions & 12 deletions mypy_protobuf/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,31 +578,43 @@ def write_stringly_typed_fields(self, desc: d.DescriptorProto) -> None:
return

if hf_fields:
wl("_HasFieldArgType: {} = {}[{}]", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), hf_fields_text)
wl(
"def HasField(self, field_name: {}[{}]) -> {}: ...",
self._import("typing", "Literal"),
hf_fields_text,
"def HasField(self, field_name: _HasFieldArgType) -> {}: ...",
self._builtin("bool"),
)
if cf_fields:
wl("_ClearFieldArgType: {} = {}[{}]", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), cf_fields_text)
wl(
"def ClearField(self, field_name: {}[{}]) -> None: ...",
self._import("typing", "Literal"),
cf_fields_text,
"def ClearField(self, field_name: _ClearFieldArgType) -> None: ...",
)

# Write type aliases first so overloads are not interrupted
for wo_field, members in sorted(wo_fields.items()):
if len(wo_fields) > 1:
wl("@{}", self._import("typing", "overload"))
wl(
"def WhichOneof(self, oneof_group: {}[{}]) -> {}[{}] | None: ...",
self._import("typing", "Literal"),
# Accepts both str and bytes
f'"{wo_field}", b"{wo_field}"',
"_WhichOneofReturnType_{}: {} = {}[{}]",
wo_field,
self._import("typing_extensions", "TypeAlias"),
self._import("typing", "Literal"),
# Returns `str`
", ".join(f'"{m}"' for m in members),
)
wl(
"_WhichOneofArgType_{}: {} = {}[{}]",
wo_field,
self._import("typing_extensions", "TypeAlias"),
self._import("typing", "Literal"),
# Accepts both str and bytes
f'"{wo_field}", b"{wo_field}"',
)
for wo_field, _ in sorted(wo_fields.items()):
if len(wo_fields) > 1:
wl("@{}", self._import("typing", "overload"))
wl(
"def WhichOneof(self, oneof_group: {}) -> {} | None: ...",
f"_WhichOneofArgType_{wo_field}",
f"_WhichOneofReturnType_{wo_field}",
)

def write_extensions(
self,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ exclude = [
executionEnvironments = [
# Due to how upb is typed, we need to disable incompatible variable override checks
{ root = "test/generated", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
{ root = "test/generated-concrete", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
{ root = "test/generated_concrete", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
{ root = "mypy_protobuf/extensions_pb2.pyi", reportIncompatibleVariableOverride = "none" },
]
17 changes: 9 additions & 8 deletions run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ MYPY_PROTOBUF_VENV=venv_$PY_VER_MYPY_PROTOBUF
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_grpc_out=test/generated

# Generate with concrete service stubs for testing
find proto -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_out=generate_concrete_servicer_stubs:test/generated-concrete
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_grpc_out=generate_concrete_servicer_stubs:test/generated-concrete
find proto -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_out=generate_concrete_servicer_stubs:test/generated_concrete
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_grpc_out=generate_concrete_servicer_stubs:test/generated_concrete


if [[ -n $VALIDATE ]] && ! diff <(echo "$SHA_BEFORE") <(find test/generated -name "*.pyi" -print0 | xargs -0 sha1sum); then
Expand All @@ -142,7 +142,8 @@ MYPY_PROTOBUF_VENV=venv_$PY_VER_MYPY_PROTOBUF
fi
)

ERRORS=()
ERROR_FILE=$(mktemp)
trap 'rm -f "$ERROR_FILE"' EXIT

for PY_VER in $PY_VER_UNIT_TESTS; do
UNIT_TESTS_VENV=venv_$PY_VER
Expand All @@ -159,7 +160,7 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
source "$MYPY_VENV"/bin/activate
# Run concrete mypy
CONCRETE_MODULES=( -m test.test_concrete )
MYPYPATH=$MYPYPATH:test/generated-concrete mypy ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${CONCRETE_MODULES[@]}"
MYPYPATH=$MYPYPATH:test/generated_concrete mypy ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${CONCRETE_MODULES[@]}"

export MYPYPATH=$MYPYPATH:test/generated

Expand Down Expand Up @@ -202,7 +203,7 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
cp "$MYPY_OUTPUT/mypy_output.omit_linenos" "test_negative/output.expected.$PY_VER_MYPY_TARGET.omit_linenos"

# Record error instead of echoing and exiting
ERRORS+=("test_negative/output.expected.$PY_VER_MYPY_TARGET didnt match. Copying over for you.")
echo "test_negative/output.expected.$PY_VER_MYPY_TARGET didnt match. Copying over for you." >> "$ERROR_FILE"
fi
)

Expand All @@ -214,11 +215,11 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
done

# Report all errors at the end
if [ ${#ERRORS[@]} -gt 0 ]; then
if [ -s "$ERROR_FILE" ]; then
echo -e "\n${RED}===============================================${NC}"
for error in "${ERRORS[@]}"; do
while IFS= read -r error; do
echo -e "${RED}$error${NC}"
done
done < "$ERROR_FILE"
echo -e "${RED}Now rerun${NC}"
exit 1
fi
7 changes: 7 additions & 0 deletions stubtest_allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,10 @@ testproto.test_extensions3_pb2.msg_option
testproto.test_extensions3_pb2.enum_option
testproto.test_extensions2_pb2.SeparateFileExtension.ext
testproto.test_pb2.Extensions1.ext


# Generated type aliases for HasField and ClearField. These do not exist on a message, but are also just type aliases
.*_HasFieldArgType
.*_ClearFieldArgType
.*_WhichOneofReturnType.*
.*_WhichOneofArgType.*
3 changes: 2 additions & 1 deletion test/generated/google/protobuf/duration_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class Duration(google.protobuf.message.Message, google.protobuf.internal.well_kn
seconds: builtins.int = ...,
nanos: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nanos", b"nanos", "seconds", b"seconds"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["nanos", b"nanos", "seconds", b"seconds"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Duration: typing_extensions.TypeAlias = Duration
3 changes: 2 additions & 1 deletion test/generated/mypy_protobuf/extensions_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class FieldOptions(google.protobuf.message.Message):
keytype: builtins.str = ...,
valuetype: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___FieldOptions: typing_extensions.TypeAlias = FieldOptions

Expand Down
15 changes: 10 additions & 5 deletions test/generated/testproto/Capitalized/Capitalized_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class lower(google.protobuf.message.Message):
*,
a: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["a", b"a"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["a", b"a"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___lower: typing_extensions.TypeAlias = lower

Expand All @@ -43,8 +44,10 @@ class Upper(google.protobuf.message.Message):
*,
Lower: Global___lower | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["Lower", b"Lower"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["Lower", b"Lower"]) -> None: ...
_HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["Lower", b"Lower"]
def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["Lower", b"Lower"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Upper: typing_extensions.TypeAlias = Upper

Expand All @@ -60,7 +63,9 @@ class lower2(google.protobuf.message.Message):
*,
upper: Global___Upper | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["upper", b"upper"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["upper", b"upper"]) -> None: ...
_HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["upper", b"upper"]
def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["upper", b"upper"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___lower2: typing_extensions.TypeAlias = lower2
3 changes: 2 additions & 1 deletion test/generated/testproto/comment_special_chars_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class Test(google.protobuf.message.Message):
j: builtins.str = ...,
k: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["a", b"a", "b", b"b", "c", b"c", "d", b"d", "e", b"e", "f", b"f", "g", b"g", "h", b"h", "i", b"i", "j", b"j", "k", b"k"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["a", b"a", "b", b"b", "c", b"c", "d", b"d", "e", b"e", "f", b"f", "g", b"g", "h", b"h", "i", b"i", "j", b"j", "k", b"k"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Test: typing_extensions.TypeAlias = Test
3 changes: 2 additions & 1 deletion test/generated/testproto/dot/com/test_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class TestMessage(google.protobuf.message.Message):
*,
foo: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["foo", b"foo"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["foo", b"foo"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___TestMessage: typing_extensions.TypeAlias = TestMessage
12 changes: 8 additions & 4 deletions test/generated/testproto/edition2024_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ class Editions2024SubMessage(google.protobuf.message.Message):
*,
thing: builtins.str | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["thing", b"thing"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["thing", b"thing"]) -> None: ...
_HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["thing", b"thing"]
def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["thing", b"thing"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Editions2024SubMessage: typing_extensions.TypeAlias = Editions2024SubMessage

Expand Down Expand Up @@ -62,7 +64,9 @@ class Editions2024Test(google.protobuf.message.Message):
implicit_singular: builtins.str = ...,
default_singular: builtins.str | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["default_singular", b"default_singular", "explicit_singular", b"explicit_singular", "legacy", b"legacy", "message_field", b"message_field"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["default_singular", b"default_singular", "explicit_singular", b"explicit_singular", "implicit_singular", b"implicit_singular", "legacy", b"legacy", "message_field", b"message_field"]) -> None: ...
_HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["default_singular", b"default_singular", "explicit_singular", b"explicit_singular", "legacy", b"legacy", "message_field", b"message_field"]
def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["default_singular", b"default_singular", "explicit_singular", b"explicit_singular", "implicit_singular", b"implicit_singular", "legacy", b"legacy", "message_field", b"message_field"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Editions2024Test: typing_extensions.TypeAlias = Editions2024Test
9 changes: 6 additions & 3 deletions test/generated/testproto/grpc/dummy_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class DummyRequest(google.protobuf.message.Message):
*,
value: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["value", b"value"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___DummyRequest: typing_extensions.TypeAlias = DummyRequest

Expand All @@ -47,7 +48,8 @@ class DummyReply(google.protobuf.message.Message):
*,
value: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["value", b"value"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___DummyReply: typing_extensions.TypeAlias = DummyReply

Expand All @@ -63,6 +65,7 @@ class DeprecatedRequest(google.protobuf.message.Message):
*,
old_field: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["old_field", b"old_field"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["old_field", b"old_field"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___DeprecatedRequest: typing_extensions.TypeAlias = DeprecatedRequest
3 changes: 2 additions & 1 deletion test/generated/testproto/inner/inner_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Inner(google.protobuf.message.Message):
*,
a: testproto.test3_pb2.OuterEnum.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["a", b"a"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["a", b"a"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Inner: typing_extensions.TypeAlias = Inner
6 changes: 4 additions & 2 deletions test/generated/testproto/nested/nested_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class Nested(google.protobuf.message.Message):
*,
a: testproto.test3_pb2.OuterEnum.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["a", b"a"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["a", b"a"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Nested: typing_extensions.TypeAlias = Nested

Expand Down Expand Up @@ -87,7 +88,8 @@ class AnotherNested(google.protobuf.message.Message):
ne: Global___AnotherNested.NestedEnum.ValueType = ...,
ne2: Global___AnotherNested.NestedMessage.NestedEnum2.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["b", b"b", "ne", b"ne", "ne2", b"ne2", "s", b"s"]) -> None: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["b", b"b", "ne", b"ne", "ne2", b"ne2", "s", b"s"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

def __init__(
self,
Expand Down
6 changes: 4 additions & 2 deletions test/generated/testproto/nopackage_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ class NoPackage2(google.protobuf.message.Message):
np: Global___NoPackage | None = ...,
np_rep: collections.abc.Iterable[Global___NoPackage] | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["np", b"np"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["np", b"np", "np_rep", b"np_rep"]) -> None: ...
_HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["np", b"np"]
def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["np", b"np", "np_rep", b"np_rep"]
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___NoPackage2: typing_extensions.TypeAlias = NoPackage2
Loading
Loading