Skip to content

Commit 505e3a0

Browse files
authored
Aidan/clear has field type aliases (#696)
* Add `_HasFieldNamesType` and `_ClearFieldNamesType` aliases to allow for typing field manipulation functions Signed-off-by: Aidan Jensen <aidandj.github@gmail.com> * Fix shellcheck errors in run_test.sh Signed-off-by: Aidan Jensen <aidandj.github@gmail.com> * Rename *NamesType to *ArgType Add WhichOneofArgType and WhichOneofReturnType aliases Signed-off-by: Aidan Jensen <aidandj.github@gmail.com> * Update CHANGELOG.md * Update README.md --------- Signed-off-by: Aidan Jensen <aidandj.github@gmail.com>
1 parent 6fa9547 commit 505e3a0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+497
-171
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ __pycache__/
88
*.pyc
99
/test/generated/**/*.py
1010
!/test/generated/**/__init__.py
11-
/test/generated-concrete/**/*.py
12-
!/test/generated-concrete/**/__init__.py
11+
/test/generated_concrete/**/*.py
12+
!/test/generated_concrete/**/__init__.py
1313
.pytest_cache
1414
/build/
1515
/dist/

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
- Protobuf <6.32 still had the edition enums and field options, so it *should* still work. But is untested
88
- Add support for editions (up to 2024)
99
- Add `generate_concrete_servicer_stubs` option to generate concrete instead of abstract servicer stubs
10+
- Add `_HasFieldArgType` and `_ClearFieldArgType` aliases to allow for typing field manipulation functions
11+
- Add `_WhichOneofArgType_<oneof_name>` and `_WhichOneofReturnType_<oneof_name>` type aliases
1012

1113
## 3.7.0
1214

README.md

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

105106
#### Examples
106107

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

374+
### `_ClearFieldArgType`, `_WhichOneofArgType_<oneof_name>`, `_WhichOneofReturnType_<oneof_name>` and `_HasFieldArgType` aliases
375+
376+
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:
377+
378+
```python
379+
from testproto.edition2024_pb2 import Editions2024Test
380+
381+
def test_hasfield_alias(msg: Editions2024Test, field: "Editions2024Test._HasFieldArgType") -> bool:
382+
return msg.HasField(field)
383+
384+
test_hasfield_alias(Editions2024Test(), "legacy")
385+
386+
def test_whichoneof_alias(
387+
msg: SimpleProto3,
388+
oneof: "SimpleProto3._WhichOneofArgType_a_oneof",
389+
) -> "SimpleProto3._WhichOneofReturnType_a_oneof | None":
390+
return msg.WhichOneof(oneof)
391+
392+
test_whichoneof_alias(SimpleProto3(), "a_oneof")
393+
```
394+
395+
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)
396+
373397
### Targeting python2 support
374398

375399
mypy-protobuf's drops support for targeting python2 with version 3.0. If you still need python2 support -

mypy_protobuf/extensions_pb2.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class FieldOptions(google.protobuf.message.Message):
3838
keytype: builtins.str = ...,
3939
valuetype: builtins.str = ...,
4040
) -> None: ...
41-
def ClearField(self, field_name: typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]) -> None: ...
41+
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]
42+
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
4243

4344
Global___FieldOptions: typing_extensions.TypeAlias = FieldOptions
4445

mypy_protobuf/main.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -578,31 +578,43 @@ def write_stringly_typed_fields(self, desc: d.DescriptorProto) -> None:
578578
return
579579

580580
if hf_fields:
581+
wl("_HasFieldArgType: {} = {}[{}]", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), hf_fields_text)
581582
wl(
582-
"def HasField(self, field_name: {}[{}]) -> {}: ...",
583-
self._import("typing", "Literal"),
584-
hf_fields_text,
583+
"def HasField(self, field_name: _HasFieldArgType) -> {}: ...",
585584
self._builtin("bool"),
586585
)
587586
if cf_fields:
587+
wl("_ClearFieldArgType: {} = {}[{}]", self._import("typing_extensions", "TypeAlias"), self._import("typing", "Literal"), cf_fields_text)
588588
wl(
589-
"def ClearField(self, field_name: {}[{}]) -> None: ...",
590-
self._import("typing", "Literal"),
591-
cf_fields_text,
589+
"def ClearField(self, field_name: _ClearFieldArgType) -> None: ...",
592590
)
593591

592+
# Write type aliases first so overloads are not interrupted
594593
for wo_field, members in sorted(wo_fields.items()):
595-
if len(wo_fields) > 1:
596-
wl("@{}", self._import("typing", "overload"))
597594
wl(
598-
"def WhichOneof(self, oneof_group: {}[{}]) -> {}[{}] | None: ...",
599-
self._import("typing", "Literal"),
600-
# Accepts both str and bytes
601-
f'"{wo_field}", b"{wo_field}"',
595+
"_WhichOneofReturnType_{}: {} = {}[{}]",
596+
wo_field,
597+
self._import("typing_extensions", "TypeAlias"),
602598
self._import("typing", "Literal"),
603599
# Returns `str`
604600
", ".join(f'"{m}"' for m in members),
605601
)
602+
wl(
603+
"_WhichOneofArgType_{}: {} = {}[{}]",
604+
wo_field,
605+
self._import("typing_extensions", "TypeAlias"),
606+
self._import("typing", "Literal"),
607+
# Accepts both str and bytes
608+
f'"{wo_field}", b"{wo_field}"',
609+
)
610+
for wo_field, _ in sorted(wo_fields.items()):
611+
if len(wo_fields) > 1:
612+
wl("@{}", self._import("typing", "overload"))
613+
wl(
614+
"def WhichOneof(self, oneof_group: {}) -> {} | None: ...",
615+
f"_WhichOneofArgType_{wo_field}",
616+
f"_WhichOneofReturnType_{wo_field}",
617+
)
606618

607619
def write_extensions(
608620
self,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ exclude = [
3939
executionEnvironments = [
4040
# Due to how upb is typed, we need to disable incompatible variable override checks
4141
{ root = "test/generated", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
42-
{ root = "test/generated-concrete", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
42+
{ root = "test/generated_concrete", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
4343
{ root = "mypy_protobuf/extensions_pb2.pyi", reportIncompatibleVariableOverride = "none" },
4444
]

run_test.sh

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ MYPY_PROTOBUF_VENV=venv_$PY_VER_MYPY_PROTOBUF
132132
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_grpc_out=test/generated
133133

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

138138

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

145-
ERRORS=()
145+
ERROR_FILE=$(mktemp)
146+
trap 'rm -f "$ERROR_FILE"' EXIT
146147

147148
for PY_VER in $PY_VER_UNIT_TESTS; do
148149
UNIT_TESTS_VENV=venv_$PY_VER
@@ -159,7 +160,7 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
159160
source "$MYPY_VENV"/bin/activate
160161
# Run concrete mypy
161162
CONCRETE_MODULES=( -m test.test_concrete )
162-
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[@]}"
163+
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[@]}"
163164

164165
export MYPYPATH=$MYPYPATH:test/generated
165166

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

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

@@ -214,11 +215,11 @@ for PY_VER in $PY_VER_UNIT_TESTS; do
214215
done
215216

216217
# Report all errors at the end
217-
if [ ${#ERRORS[@]} -gt 0 ]; then
218+
if [ -s "$ERROR_FILE" ]; then
218219
echo -e "\n${RED}===============================================${NC}"
219-
for error in "${ERRORS[@]}"; do
220+
while IFS= read -r error; do
220221
echo -e "${RED}$error${NC}"
221-
done
222+
done < "$ERROR_FILE"
222223
echo -e "${RED}Now rerun${NC}"
223224
exit 1
224225
fi

stubtest_allowlist.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,10 @@ testproto.test_extensions3_pb2.msg_option
239239
testproto.test_extensions3_pb2.enum_option
240240
testproto.test_extensions2_pb2.SeparateFileExtension.ext
241241
testproto.test_pb2.Extensions1.ext
242+
243+
244+
# Generated type aliases for HasField and ClearField. These do not exist on a message, but are also just type aliases
245+
.*_HasFieldArgType
246+
.*_ClearFieldArgType
247+
.*_WhichOneofReturnType.*
248+
.*_WhichOneofArgType.*

test/generated/google/protobuf/duration_pb2.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class Duration(google.protobuf.message.Message, google.protobuf.internal.well_kn
131131
seconds: builtins.int = ...,
132132
nanos: builtins.int = ...,
133133
) -> None: ...
134-
def ClearField(self, field_name: typing.Literal["nanos", b"nanos", "seconds", b"seconds"]) -> None: ...
134+
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["nanos", b"nanos", "seconds", b"seconds"]
135+
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
135136

136137
Global___Duration: typing_extensions.TypeAlias = Duration

test/generated/mypy_protobuf/extensions_pb2.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class FieldOptions(google.protobuf.message.Message):
3838
keytype: builtins.str = ...,
3939
valuetype: builtins.str = ...,
4040
) -> None: ...
41-
def ClearField(self, field_name: typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]) -> None: ...
41+
_ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["casttype", b"casttype", "keytype", b"keytype", "valuetype", b"valuetype"]
42+
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
4243

4344
Global___FieldOptions: typing_extensions.TypeAlias = FieldOptions
4445

0 commit comments

Comments
 (0)