Skip to content
Open
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
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ __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
/test/generated_async_only/**/*.py
!/test/generated_async_only/**/__init__.py
/test/generated_sync_only/**/*.py
!/test/generated_sync_only/**/__init__.py
.pytest_cache
/build/
/dist/
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,21 @@ By default mypy-protobuf will output servicer stubs with abstract methods. To ou
protoc --python_out=output/location --mypy_grpc_out=generate_concrete_servicer_stubs:output/location
```

### `sync_only/async_only`

By default, generated GRPC stubs are compatible with both sync and async variants. If you only
want sync or async GRPC stubs, use this option:

```
protoc --python_out=output/location --mypy_grpc_out=sync_only:output/location
```

or

```
protoc --python_out=output/location --mypy_grpc_out=async_only:output/location
```

### Output suppression

To suppress output, you can run
Expand Down
283 changes: 199 additions & 84 deletions mypy_protobuf/main.py

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ line-length = 10000
[tool.isort]
profile = "black"
skip_gitignore = true
extend_skip_glob = ["*_pb2.py"]
extend_skip_glob = ["*_pb2.py", "*_pb2_grpc.py"]

[tool.mypy]
strict = true
Expand All @@ -33,12 +33,16 @@ include = [
exclude = [
"**/*_pb2.py",
"**/*_pb2_grpc.py",
"test/test_concrete.py"
"test/test_concrete.py",
]

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 = "test/generated_sync_only", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
{ root = "test/generated_async_only", extraPaths = ["./"], reportIncompatibleVariableOverride = "none" },
{ root = "mypy_protobuf/extensions_pb2.pyi", reportIncompatibleVariableOverride = "none" },
{ root = "test/async_only", extraPaths = ["test/generated_async_only"] },
{ root = "test/sync_only", extraPaths = ["test/generated_sync_only"] },
]
23 changes: 19 additions & 4 deletions run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,14 @@
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

# Generate with sync_only stubs for testing
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_grpc_out=only_sync:test/generated_sync_only --mypy_out=test/generated_sync_only --python_out=test/generated_sync_only

# Generate with async_only stubs for testing
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 "$PROTOC" "${PROTOC_ARGS[@]}" --mypy_grpc_out=only_async:test/generated_async_only --mypy_out=test/generated_async_only --python_out=test/generated_async_only

if [[ -n $VALIDATE ]] && ! diff <(echo "$SHA_BEFORE") <(find test/generated -name "*.pyi" -print0 | xargs -0 sha1sum); then
echo -e "${RED}Some .pyi files did not match. Please commit those files${NC}"
Expand All @@ -152,14 +157,24 @@
(
source "$UNIT_TESTS_VENV"/bin/activate
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 python -m grpc_tools.protoc "${PROTOC_ARGS[@]}" --grpc_python_out=test/generated
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 python -m grpc_tools.protoc "${PROTOC_ARGS[@]}" --grpc_python_out=test/generated_sync_only
find proto/testproto/grpc -name "*.proto" -print0 | xargs -0 python -m grpc_tools.protoc "${PROTOC_ARGS[@]}" --grpc_python_out=test/generated_async_only
)

# Run mypy on unit tests / generated output
(
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[@]}"

# Run sync_only mypy
SYNC_ONLY_MODULES=( -m test.sync_only.test_sync_only )
MYPYPATH=$MYPYPATH:test/generated_sync_only mypy ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${SYNC_ONLY_MODULES[@]}"

# Run async_only mypy
ASYNC_ONLY_MODULES=( -m test.async_only.test_async_only )
MYPYPATH=$MYPYPATH:test/generated_async_only mypy ${CUSTOM_TYPESHED_DIR_ARG:+"$CUSTOM_TYPESHED_DIR_ARG"} --python-executable="$UNIT_TESTS_VENV"/bin/python --python-version="$PY_VER_MYPY_TARGET" "${ASYNC_ONLY_MODULES[@]}"

export MYPYPATH=$MYPYPATH:test/generated

Expand Down Expand Up @@ -202,21 +217,21 @@
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.")

Check warning on line 220 in run_test.sh

View workflow job for this annotation

GitHub Actions / Linting

[shellcheck] reported by reviewdog 🐶 Modification of ERRORS is local (to subshell caused by (..) group). Raw Output: ./run_test.sh:220:13: info: Modification of ERRORS is local (to subshell caused by (..) group). (ShellCheck.SC2030)
fi
)

(
# Run unit tests.
source "$UNIT_TESTS_VENV"/bin/activate
PYTHONPATH=test/generated py.test --ignore=test/generated -v
PYTHONPATH=test/generated py.test --ignore=test/generated --ignore=test/generated_sync_only --ignore=test/generated_async_only -v
)
done

# Report all errors at the end
if [ ${#ERRORS[@]} -gt 0 ]; then

Check warning on line 232 in run_test.sh

View workflow job for this annotation

GitHub Actions / Linting

[shellcheck] reported by reviewdog 🐶 ERRORS was modified in a subshell. That change might be lost. Raw Output: ./run_test.sh:232:6: info: ERRORS was modified in a subshell. That change might be lost. (ShellCheck.SC2031)
echo -e "\n${RED}===============================================${NC}"
for error in "${ERRORS[@]}"; do

Check warning on line 234 in run_test.sh

View workflow job for this annotation

GitHub Actions / Linting

[shellcheck] reported by reviewdog 🐶 ERRORS was modified in a subshell. That change might be lost. Raw Output: ./run_test.sh:234:19: info: ERRORS was modified in a subshell. That change might be lost. (ShellCheck.SC2031)
echo -e "${RED}$error${NC}"
done
echo -e "${RED}Now rerun${NC}"
Expand Down
31 changes: 31 additions & 0 deletions test/async_only/test_async_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Type-checking test for async_only GRPC stubs.

This module is run through mypy to validate that stubs generated with the
only_async flag have the correct types:
- Regular (non-generic) Stub class that only accepts grpc.aio.Channel
- No AsyncStub type alias (the stub itself is async-only)
- Servicer methods use AsyncIterator for client streaming (not _MaybeAsyncIterator)
- add_XXXServicer_to_server accepts grpc.aio.Server
"""

from typing import Awaitable

import grpc.aio
from testproto.grpc import dummy_pb2, dummy_pb2_grpc

Check warning on line 15 in test/async_only/test_async_only.py

View workflow job for this annotation

GitHub Actions / Pyright - 3.14.0

Import "testproto.grpc.dummy_pb2_grpc" could not be resolved from source (reportMissingModuleSource)

Check warning on line 15 in test/async_only/test_async_only.py

View workflow job for this annotation

GitHub Actions / Pyright - 3.14.0

Import "testproto.grpc.dummy_pb2" could not be resolved from source (reportMissingModuleSource)

Check warning on line 15 in test/async_only/test_async_only.py

View workflow job for this annotation

GitHub Actions / Pyright - 3.13.9

Import "testproto.grpc.dummy_pb2_grpc" could not be resolved from source (reportMissingModuleSource)

Check warning on line 15 in test/async_only/test_async_only.py

View workflow job for this annotation

GitHub Actions / Pyright - 3.13.9

Import "testproto.grpc.dummy_pb2" could not be resolved from source (reportMissingModuleSource)


class AsyncOnlyServicer(dummy_pb2_grpc.DummyServiceServicer):
async def UnaryUnary(
self,
request: dummy_pb2.DummyRequest,
context: grpc.aio.ServicerContext[dummy_pb2.DummyRequest, Awaitable[dummy_pb2.DummyReply]],
) -> dummy_pb2.DummyReply:
await context.abort(grpc.StatusCode.UNIMPLEMENTED, "Not implemented")
return dummy_pb2.DummyReply(value=request.value[::-1])


async def noop() -> None:
"""Don't actually run anything; this is just for type-checking."""
stub = dummy_pb2_grpc.DummyServiceStub(channel=grpc.aio.insecure_channel("localhost:50051"))
await stub.UnaryUnary(dummy_pb2.DummyRequest(value="test"))
Empty file.
Empty file.
137 changes: 137 additions & 0 deletions test/generated_async_only/testproto/grpc/dummy_pb2_grpc.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
https://github.com/vmagamedov/grpclib/blob/master/tests/dummy.proto"""

import abc
import collections.abc
import grpc.aio
import sys
import testproto.grpc.dummy_pb2
import typing

if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions

if sys.version_info >= (3, 13):
from warnings import deprecated
else:
from typing_extensions import deprecated


GRPC_GENERATED_VERSION: str
GRPC_VERSION: str
_DummyServiceUnaryUnaryType: typing_extensions.TypeAlias = grpc.aio.UnaryUnaryMultiCallable[
testproto.grpc.dummy_pb2.DummyRequest,
testproto.grpc.dummy_pb2.DummyReply,
]
_DummyServiceUnaryStreamType: typing_extensions.TypeAlias = grpc.aio.UnaryStreamMultiCallable[
testproto.grpc.dummy_pb2.DummyRequest,
testproto.grpc.dummy_pb2.DummyReply,
]
_DummyServiceStreamUnaryType: typing_extensions.TypeAlias = grpc.aio.StreamUnaryMultiCallable[
testproto.grpc.dummy_pb2.DummyRequest,
testproto.grpc.dummy_pb2.DummyReply,
]
_DummyServiceStreamStreamType: typing_extensions.TypeAlias = grpc.aio.StreamStreamMultiCallable[
testproto.grpc.dummy_pb2.DummyRequest,
testproto.grpc.dummy_pb2.DummyReply,
]
class DummyServiceStub:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing typed constructor, in sync as well

def __init__(channel: grpc.aio.Channel)

"""DummyService"""

def __init__(self, channel: grpc.aio.Channel) -> None: ...

UnaryUnary: _DummyServiceUnaryUnaryType
"""UnaryUnary"""

UnaryStream: _DummyServiceUnaryStreamType
"""UnaryStream"""

StreamUnary: _DummyServiceStreamUnaryType
"""StreamUnary"""

StreamStream: _DummyServiceStreamStreamType
"""StreamStream"""

class DummyServiceServicer(metaclass=abc.ABCMeta):
"""DummyService"""

@abc.abstractmethod
def UnaryUnary(
self,
request: testproto.grpc.dummy_pb2.DummyRequest,
context: grpc.aio.ServicerContext[testproto.grpc.dummy_pb2.DummyRequest, collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]],
) -> collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]:
"""UnaryUnary"""

@abc.abstractmethod
def UnaryStream(
self,
request: testproto.grpc.dummy_pb2.DummyRequest,
context: grpc.aio.ServicerContext[testproto.grpc.dummy_pb2.DummyRequest, collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyReply]],
) -> collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyReply]:
"""UnaryStream"""

@abc.abstractmethod
def StreamUnary(
self,
request_iterator: collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyRequest],
context: grpc.aio.ServicerContext[collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyRequest], collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]],
) -> collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]:
"""StreamUnary"""

@abc.abstractmethod
def StreamStream(
self,
request_iterator: collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyRequest],
context: grpc.aio.ServicerContext[collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyRequest], collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyReply]],
) -> collections.abc.AsyncIterator[testproto.grpc.dummy_pb2.DummyReply]:
"""StreamStream"""

def add_DummyServiceServicer_to_server(servicer: DummyServiceServicer, server: grpc.aio.Server) -> None: ...

_DeprecatedServiceDeprecatedMethodType: typing_extensions.TypeAlias = grpc.aio.UnaryUnaryMultiCallable[
testproto.grpc.dummy_pb2.DeprecatedRequest,
testproto.grpc.dummy_pb2.DummyReply,
]
_DeprecatedServiceDeprecatedMethodNotDeprecatedRequestType: typing_extensions.TypeAlias = grpc.aio.UnaryUnaryMultiCallable[
testproto.grpc.dummy_pb2.DummyRequest,
testproto.grpc.dummy_pb2.DummyReply,
]
@deprecated("""This service is deprecated""")
class DeprecatedServiceStub:
"""Marking the service as deprecated"""

def __init__(self, channel: grpc.aio.Channel) -> None: ...

DeprecatedMethod: _DeprecatedServiceDeprecatedMethodType
"""DeprecatedMethod"""

DeprecatedMethodNotDeprecatedRequest: _DeprecatedServiceDeprecatedMethodNotDeprecatedRequestType
"""DeprecatedMethodNotDeprecatedRequest"""

@deprecated("""This service is deprecated""")
class DeprecatedServiceServicer(metaclass=abc.ABCMeta):
"""Marking the service as deprecated"""

@abc.abstractmethod
def DeprecatedMethod(
self,
request: testproto.grpc.dummy_pb2.DeprecatedRequest,
context: grpc.aio.ServicerContext[testproto.grpc.dummy_pb2.DeprecatedRequest, collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]],
) -> collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]:
"""DeprecatedMethod"""

@abc.abstractmethod
def DeprecatedMethodNotDeprecatedRequest(
self,
request: testproto.grpc.dummy_pb2.DummyRequest,
context: grpc.aio.ServicerContext[testproto.grpc.dummy_pb2.DummyRequest, collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]],
) -> collections.abc.Awaitable[testproto.grpc.dummy_pb2.DummyReply]:
"""DeprecatedMethodNotDeprecatedRequest"""

@deprecated("""This service is deprecated""")
def add_DeprecatedServiceServicer_to_server(servicer: DeprecatedServiceServicer, server: grpc.aio.Server) -> None: ...
73 changes: 73 additions & 0 deletions test/generated_async_only/testproto/grpc/import_pb2_grpc.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""

import abc
import collections.abc
import google.protobuf.empty_pb2
import grpc.aio
import sys
import testproto.test_pb2

Check failure on line 11 in test/generated_async_only/testproto/grpc/import_pb2_grpc.pyi

View workflow job for this annotation

GitHub Actions / Pyright - 3.14.0

Import "testproto.test_pb2" could not be resolved (reportMissingImports)

Check failure on line 11 in test/generated_async_only/testproto/grpc/import_pb2_grpc.pyi

View workflow job for this annotation

GitHub Actions / Pyright - 3.13.9

Import "testproto.test_pb2" could not be resolved (reportMissingImports)
import typing

if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions


GRPC_GENERATED_VERSION: str
GRPC_VERSION: str
_SimpleServiceUnaryUnaryType: typing_extensions.TypeAlias = grpc.aio.UnaryUnaryMultiCallable[
google.protobuf.empty_pb2.Empty,
testproto.test_pb2.Simple1,
]
_SimpleServiceUnaryStreamType: typing_extensions.TypeAlias = grpc.aio.UnaryUnaryMultiCallable[
testproto.test_pb2.Simple1,
google.protobuf.empty_pb2.Empty,
]
_SimpleServiceNoCommentType: typing_extensions.TypeAlias = grpc.aio.UnaryUnaryMultiCallable[
testproto.test_pb2.Simple1,
google.protobuf.empty_pb2.Empty,
]
class SimpleServiceStub:
"""SimpleService"""

def __init__(self, channel: grpc.aio.Channel) -> None: ...

UnaryUnary: _SimpleServiceUnaryUnaryType
"""UnaryUnary"""

UnaryStream: _SimpleServiceUnaryStreamType
"""UnaryStream"""

NoComment: _SimpleServiceNoCommentType

class SimpleServiceServicer(metaclass=abc.ABCMeta):
"""SimpleService"""

@abc.abstractmethod
def UnaryUnary(
self,
request: google.protobuf.empty_pb2.Empty,
context: grpc.aio.ServicerContext[google.protobuf.empty_pb2.Empty, collections.abc.Awaitable[testproto.test_pb2.Simple1]],
) -> collections.abc.Awaitable[testproto.test_pb2.Simple1]:
"""UnaryUnary"""

@abc.abstractmethod
def UnaryStream(
self,
request: testproto.test_pb2.Simple1,
context: grpc.aio.ServicerContext[testproto.test_pb2.Simple1, collections.abc.Awaitable[google.protobuf.empty_pb2.Empty]],
) -> collections.abc.Awaitable[google.protobuf.empty_pb2.Empty]:
"""UnaryStream"""

@abc.abstractmethod
def NoComment(
self,
request: testproto.test_pb2.Simple1,
context: grpc.aio.ServicerContext[testproto.test_pb2.Simple1, collections.abc.Awaitable[google.protobuf.empty_pb2.Empty]],
) -> collections.abc.Awaitable[google.protobuf.empty_pb2.Empty]: ...

def add_SimpleServiceServicer_to_server(servicer: SimpleServiceServicer, server: grpc.aio.Server) -> None: ...
Loading
Loading