diff --git a/.gitignore b/.gitignore index 18a5a134..829efc43 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 446f303a..4b7866d6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/mypy_protobuf/main.py b/mypy_protobuf/main.py index 487f30f5..d05cb288 100644 --- a/mypy_protobuf/main.py +++ b/mypy_protobuf/main.py @@ -3,6 +3,7 @@ from __future__ import annotations import ast +import enum import sys from collections import defaultdict from contextlib import contextmanager @@ -119,9 +120,9 @@ def _add_enums( prefix: str, _fd: d.FileDescriptorProto, ) -> None: - for enum in enums: - self.message_to_fd[prefix + enum.name] = _fd - self.message_to_fd[prefix + enum.name + ".ValueType"] = _fd + for enum_proto in enums: + self.message_to_fd[prefix + enum_proto.name] = _fd + self.message_to_fd[prefix + enum_proto.name + ".ValueType"] = _fd def _add_messages( messages: "RepeatedCompositeFieldContainer[d.DescriptorProto]", @@ -141,6 +142,29 @@ def _add_messages( _add_enums(fd.enum_type, start_prefix, fd) +class GRPCType(enum.Enum): + SYNC = "SYNC" + ASYNC = "ASYNC" + BOTH = "BOTH" + + @classmethod + def from_parameter(cls, parameter: str) -> GRPCType: + if "only_sync" in parameter: + return GRPCType.SYNC + elif "only_async" in parameter: + return GRPCType.ASYNC + else: + return GRPCType.BOTH + + @property + def supports_sync(self) -> bool: + return self in (GRPCType.SYNC, GRPCType.BOTH) + + @property + def supports_async(self) -> bool: + return self in (GRPCType.ASYNC, GRPCType.BOTH) + + class PkgWriter(object): """Writes a single pyi file""" @@ -153,6 +177,7 @@ def __init__( use_default_deprecation_warnings: bool, generate_concrete_servicer_stubs: bool, grpc: bool, + grpc_type: GRPCType = GRPCType.BOTH, ) -> None: self.fd = fd self.descriptors = descriptors @@ -161,6 +186,7 @@ def __init__( self.use_default_depreaction_warnings = use_default_deprecation_warnings self.generate_concrete_servicer_stubs = generate_concrete_servicer_stubs self.grpc = grpc + self.grpc_type = grpc_type self.lines: List[str] = [] self.indent = "" @@ -263,7 +289,7 @@ def _has_comments(self, scl: SourceCodeLocation) -> bool: return sci_loc is not None and bool(sci_loc.leading_detached_comments or sci_loc.leading_comments or sci_loc.trailing_comments) def _get_comments(self, scl: SourceCodeLocation) -> List[str]: - """Return list of comment lines""" + """Return a list of comment lines""" if not self._has_comments(scl): return [] @@ -385,12 +411,12 @@ def write_enums( scl_prefix: SourceCodeLocation, ) -> None: wl = self._write_line - for i, enum in enumerate(enums): - class_name = enum.name if enum.name not in PYTHON_RESERVED else "_r_" + enum.name + for i, enum_proto in enumerate(enums): + class_name = enum_proto.name if enum_proto.name not in PYTHON_RESERVED else "_r_" + enum_proto.name value_type_fq = prefix + class_name + ".ValueType" - enum_helper_class = "_" + enum.name + enum_helper_class = "_" + enum_proto.name value_type_helper_fq = prefix + enum_helper_class + ".ValueType" - etw_helper_class = "_" + enum.name + "EnumTypeWrapper" + etw_helper_class = "_" + enum_proto.name + "EnumTypeWrapper" scl = scl_prefix + [i] wl(f"class {enum_helper_class}:") @@ -414,13 +440,13 @@ def write_enums( ed = self._import("google.protobuf.descriptor", "EnumDescriptor") wl(f"DESCRIPTOR: {ed}") self.write_enum_values( - [(i, v) for i, v in enumerate(enum.value) if v.name not in PROTO_ENUM_RESERVED], + [(i, v) for i, v in enumerate(enum_proto.value) if v.name not in PROTO_ENUM_RESERVED], value_type_helper_fq, scl + [d.EnumDescriptorProto.VALUE_FIELD_NUMBER], ) wl("") - if enum.options.deprecated: + if enum_proto.options.deprecated: self._write_deprecation_warning( scl + [d.EnumDescriptorProto.OPTIONS_FIELD_NUMBER] + [d.EnumOptions.DEPRECATED_FIELD_NUMBER], "This enum has been marked as deprecated using proto enum options.", @@ -436,7 +462,7 @@ def write_enums( wl("") self.write_enum_values( - enumerate(enum.value), + enumerate(enum_proto.value), value_type_fq, scl + [d.EnumDescriptorProto.VALUE_FIELD_NUMBER], ) @@ -755,8 +781,13 @@ def _input_type(self, method: d.MethodDescriptorProto) -> str: def _servicer_input_type(self, method: d.MethodDescriptorProto) -> str: result = self._import_message(method.input_type) if method.client_streaming: - # See write_grpc_async_hacks(). - result = f"_MaybeAsyncIterator[{result}]" + # See write_grpc_iterator_type(). + if self.grpc_type == GRPCType.SYNC: + result = f'{self._import("collections.abc", "Iterator")}[{result}]' + elif self.grpc_type == GRPCType.ASYNC: + result = f'{self._import("collections.abc", "AsyncIterator")}[{result}]' + else: + result = f"_MaybeAsyncIterator[{result}]" return result def _output_type(self, method: d.MethodDescriptorProto) -> str: @@ -768,33 +799,64 @@ def _servicer_output_type(self, method: d.MethodDescriptorProto) -> str: if method.server_streaming: # Union[Iterator[Resp], AsyncIterator[Resp]] is subtyped by Iterator[Resp] and AsyncIterator[Resp]. # So both can be used in the covariant function return position. - iterator = f"{self._import('collections.abc', 'Iterator')}[{result}]" - aiterator = f"{self._import('collections.abc', 'AsyncIterator')}[{result}]" - result = f"{self._import('typing', 'Union')}[{iterator}, {aiterator}]" + sync = f"{self._import('collections.abc', 'Iterator')}[{result}]" + a_sync = f"{self._import('collections.abc', 'AsyncIterator')}[{result}]" + result = f"{self._import('typing', 'Union')}[{sync}, {a_sync}]" else: # Union[Resp, Awaitable[Resp]] is subtyped by Resp and Awaitable[Resp]. # So both can be used in the covariant function return position. # Awaitable[Resp] is equivalent to async def. - awaitable = f"{self._import('collections.abc', 'Awaitable')}[{result}]" - result = f"{self._import('typing', 'Union')}[{result}, {awaitable}]" - return result + sync = result + a_sync = f"{self._import('collections.abc', 'Awaitable')}[{result}]" + result = f"{self._import('typing', 'Union')}[{sync}, {a_sync}]" + + if self.grpc_type == GRPCType.SYNC: + return sync + elif self.grpc_type == GRPCType.ASYNC: + return a_sync + else: + return result - def write_grpc_async_hacks(self) -> None: + def write_grpc_iterator_type(self) -> None: wl = self._write_line - # _MaybeAsyncIterator[Req] is supertyped by Iterator[Req] and AsyncIterator[Req]. - # So both can be used in the contravariant function parameter position. - wl('_T = {}("_T")', self._import("typing", "TypeVar")) - wl("") - wl( - "class _MaybeAsyncIterator({}[_T], {}[_T], metaclass={}): ...", - self._import("collections.abc", "AsyncIterator"), - self._import("collections.abc", "Iterator"), - self._import("abc", "ABCMeta"), - ) + + if self.grpc_type == GRPCType.SYNC: + self._import("collections.abc", "Iterator") + elif self.grpc_type == GRPCType.ASYNC: + self._import("collections.abc", "AsyncIterator") + else: + # _MaybeAsyncIterator[Req] is supertyped by Iterator[Req] and AsyncIterator[Req]. + # So both can be used in the contravariant function parameter position. + wl('_T = {}("_T")', self._import("typing", "TypeVar")) + wl("") + wl( + "class _MaybeAsyncIterator({}[_T], {}[_T], metaclass={}): ...", + self._import("collections.abc", "AsyncIterator"), + self._import("collections.abc", "Iterator"), + self._import("abc", "ABCMeta"), + ) wl("") - # _ServicerContext is supertyped by grpc.ServicerContext and grpc.aio.ServicerContext - # So both can be used in the contravariant function parameter position. + def get_servicer_context_type(self, input_: str, output: str) -> str: + """Get the type to use for the context parameter in servicer methods.""" + if self.grpc_type == GRPCType.ASYNC: + return self._import("grpc.aio", f"ServicerContext[{input_}, {output}]") + elif self.grpc_type == GRPCType.SYNC: + return self._import("grpc", "ServicerContext") + else: + # BOTH mode uses _ServicerContext union class + return "_ServicerContext" + + def write_grpc_servicer_context(self) -> None: + """Write _ServicerContext class only for BOTH mode (union type needed).""" + wl = self._write_line + + if self.grpc_type != GRPCType.BOTH: + return + + # BOTH mode: _ServicerContext is a union class that's supertyped by both + # grpc.ServicerContext and grpc.aio.ServicerContext, so both can be used + # in the contravariant function parameter position. wl( "class _ServicerContext({}, {}): # type: ignore[misc, type-arg]", self._import("grpc", "ServicerContext"), @@ -810,26 +872,40 @@ def write_grpc_type_vars(self, service: d.ServiceDescriptorProto) -> None: if not methods: return for _, method in methods: - wl("{} = {}(", _build_typevar_name(service.name, method.name), self._import("typing_extensions", "TypeVar")) - with self._indent(): - wl("'{}',", _build_typevar_name(service.name, method.name)) - wl("{}[", self._callable_type(method, is_async=False)) + # Write only a sync of an async version + if self.grpc_type != GRPCType.BOTH: + type_var = _build_typevar_name(service.name, method.name) + callable_type = self._callable_type(method, is_async=self.grpc_type == GRPCType.ASYNC) + wl("{}: {} = {}[", type_var, self._import("typing_extensions", "TypeAlias"), callable_type) with self._indent(): wl("{},", self._input_type(method)) wl("{},", self._output_type(method)) - wl("],") - wl("{}[", self._callable_type(method, is_async=True)) - with self._indent(): - wl("{},", self._input_type(method)) - wl("{},", self._output_type(method)) - wl("],") - wl("default={}[", self._callable_type(method, is_async=False)) + wl("]") + + # Write both sync and async as a TypeVar + else: + wl("{} = {}(", _build_typevar_name(service.name, method.name), self._import("typing_extensions", "TypeVar")) with self._indent(): - wl("{},", self._input_type(method)) - wl("{},", self._output_type(method)) - wl("],") - wl(")") - wl("") + wl("'{}',", _build_typevar_name(service.name, method.name)) + wl("{}[", self._callable_type(method, is_async=False)) + with self._indent(): + wl("{},", self._input_type(method)) + wl("{},", self._output_type(method)) + wl("],") + + wl("{}[", self._callable_type(method, is_async=True)) + with self._indent(): + wl("{},", self._input_type(method)) + wl("{},", self._output_type(method)) + wl("],") + + wl("default={}[", self._callable_type(method, is_async=False)) + with self._indent(): + wl("{},", self._input_type(method)) + wl("{},", self._output_type(method)) + wl("],") + wl(")") + wl("") def write_self_types(self, service: d.ServiceDescriptorProto, is_async: bool) -> None: wl = self._write_line @@ -852,6 +928,8 @@ def write_grpc_methods(self, service: d.ServiceDescriptorProto, scl_prefix: Sour wl("") for i, method in methods: scl = scl_prefix + [d.ServiceDescriptorProto.METHOD_FIELD_NUMBER, i] + input_type = self._servicer_input_type(method) + output_type = self._servicer_output_type(method) if self.generate_concrete_servicer_stubs is False: wl("@{}", self._import("abc", "abstractmethod")) @@ -859,12 +937,11 @@ def write_grpc_methods(self, service: d.ServiceDescriptorProto, scl_prefix: Sour with self._indent(): wl("self,") input_name = "request_iterator" if method.client_streaming else "request" - input_type = self._servicer_input_type(method) wl(f"{input_name}: {input_type},") - wl("context: _ServicerContext,") + wl("context: {},", self.get_servicer_context_type(input_type, output_type)) wl( ") -> {}:{}", - self._servicer_output_type(method), + output_type, " ..." if not self._has_comments(scl) else "", ) if self._has_comments(scl): @@ -910,43 +987,62 @@ def write_grpc_services( "This stub has been marked as deprecated using proto service options.", ) class_name = f"{service.name}Stub" - wl( - "class {}({}[{}]):", - class_name, - self._import("typing", "Generic"), - ", ".join(f"{_build_typevar_name(service.name, method.name)}" for method in service.method), - ) + + if self.grpc_type == GRPCType.BOTH: + wl( + "class {}({}[{}]):", + class_name, + self._import("typing", "Generic"), + ", ".join(f"{_build_typevar_name(service.name, method.name)}" for method in service.method), + ) + else: + wl(f"class {class_name}:") + with self._indent(): if self._write_comments(scl): wl("") - # Write sync overload - wl("@{}", self._import("typing", "overload")) - wl("def __init__(self: {}[", class_name) - self.write_self_types(service, False) - wl( - "], channel: {}) -> None: ...", - self._import("grpc", "Channel"), - ) - wl("") + if self.grpc_type == GRPCType.BOTH: + # Write sync overload + wl("@{}", self._import("typing", "overload")) + wl("def __init__(self: {}[", class_name) + self.write_self_types(service, is_async=False) + wl( + "], channel: {}) -> None: ...", + self._import("grpc", "Channel"), + ) + wl("") - # Write async overload - wl("@{}", self._import("typing", "overload")) - wl("def __init__(self: {}[", class_name) - self.write_self_types(service, True) - wl( - "], channel: {}) -> None: ...", - self._import("grpc.aio", "Channel"), - ) - wl("") + # Write async overload + wl("@{}", self._import("typing", "overload")) + wl("def __init__(self: {}[", class_name) + self.write_self_types(service, is_async=True) + wl( + "], channel: {}) -> None: ...", + self._import("grpc.aio", "Channel"), + ) + wl("") + elif self.grpc_type == GRPCType.SYNC: + wl( + "def __init__(self, channel: {}) -> None: ...", + self._import("grpc", "Channel"), + ) + wl("") + elif self.grpc_type == GRPCType.ASYNC: + wl( + "def __init__(self, channel: {}) -> None: ...", + self._import("grpc.aio", "Channel"), + ) + wl("") self.write_grpc_stub_methods(service, scl) - # Write AsyncStub alias - wl("{}AsyncStub: {} = {}[", service.name, self._import("typing_extensions", "TypeAlias"), class_name) - self.write_self_types(service, True) - wl("]") - wl("") + if self.grpc_type == GRPCType.BOTH and self.grpc_type.supports_async: + # Write AsyncStub alias (only when Stub is generic) + wl("{}AsyncStub: {} = {}[", service.name, self._import("typing_extensions", "TypeAlias"), class_name) + self.write_self_types(service, is_async=True) + wl("]") + wl("") # The service definition interface if service.options.deprecated: @@ -969,21 +1065,36 @@ def write_grpc_services( if self._write_comments(scl): wl("") self.write_grpc_methods(service, scl) - server = self._import("grpc", "Server") - aserver = self._import("grpc.aio", "Server") + if service.options.deprecated: self._write_deprecation_warning( scl + [d.ServiceDescriptorProto.OPTIONS_FIELD_NUMBER] + [d.ServiceOptions.DEPRECATED_FIELD_NUMBER], "This servicer has been marked as deprecated using proto service options.", ) + wl( "def add_{}Servicer_to_server(servicer: {}Servicer, server: {}) -> None: ...", service.name, service.name, - f"{self._import('typing', 'Union')}[{server}, {aserver}]", + self.make_server_type(), ) wl("") + def make_server_type(self) -> str: + server = aserver = None + if self.grpc_type.supports_sync: + server = self._import("grpc", "Server") + if self.grpc_type.supports_async: + aserver = self._import("grpc.aio", "Server") + + if server and aserver: + return f"{self._import('typing', 'Union')}[{server}, {aserver}]" + elif server: + return server + elif aserver: + return aserver + raise RuntimeError(f"Impossible, {self.grpc_type=}") # pragma: no cover + def python_type(self, field: d.FieldDescriptorProto, generic_container: bool = False) -> str: """ generic_container @@ -1171,6 +1282,7 @@ def generate_mypy_grpc_stubs( relax_strict_optional_primitives: bool, use_default_deprecation_warnings: bool, generate_concrete_servicer_stubs: bool, + grpc_type: GRPCType, ) -> None: for name, fd in descriptors.to_generate.items(): pkg_writer = PkgWriter( @@ -1181,8 +1293,10 @@ def generate_mypy_grpc_stubs( use_default_deprecation_warnings, generate_concrete_servicer_stubs, grpc=True, + grpc_type=grpc_type, ) - pkg_writer.write_grpc_async_hacks() + pkg_writer.write_grpc_iterator_type() + pkg_writer.write_grpc_servicer_context() pkg_writer.write_grpc_services(fd.service, [d.FileDescriptorProto.SERVICE_FIELD_NUMBER]) assert name == fd.name @@ -1251,6 +1365,7 @@ def grpc() -> None: "relax_strict_optional_primitives" in request.parameter, "use_default_deprecation_warnings" in request.parameter, "generate_concrete_servicer_stubs" in request.parameter, + GRPCType.from_parameter(request.parameter), ) diff --git a/pyproject.toml b/pyproject.toml index ff1a16d9..19be1c3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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"] }, ] diff --git a/run_test.sh b/run_test.sh index f6426ac6..c2f6e0c8 100755 --- a/run_test.sh +++ b/run_test.sh @@ -132,9 +132,14 @@ 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 + # 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}" @@ -152,6 +157,8 @@ for PY_VER in $PY_VER_UNIT_TESTS; do ( 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 @@ -159,7 +166,15 @@ 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[@]}" + + # 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 @@ -209,7 +224,7 @@ for PY_VER in $PY_VER_UNIT_TESTS; do ( # 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 diff --git a/test/async_only/test_async_only.py b/test/async_only/test_async_only.py new file mode 100644 index 00000000..5dc5b22c --- /dev/null +++ b/test/async_only/test_async_only.py @@ -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 + + +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")) diff --git a/test/generated_async_only/testproto/__init__.py b/test/generated_async_only/testproto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/generated_async_only/testproto/grpc/__init__.py b/test/generated_async_only/testproto/grpc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/generated-concrete/testproto/grpc/dummy_pb2.pyi b/test/generated_async_only/testproto/grpc/dummy_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/grpc/dummy_pb2.pyi rename to test/generated_async_only/testproto/grpc/dummy_pb2.pyi diff --git a/test/generated_async_only/testproto/grpc/dummy_pb2_grpc.pyi b/test/generated_async_only/testproto/grpc/dummy_pb2_grpc.pyi new file mode 100644 index 00000000..02a1c6a8 --- /dev/null +++ b/test/generated_async_only/testproto/grpc/dummy_pb2_grpc.pyi @@ -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: + """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: ... diff --git a/test/generated-concrete/testproto/grpc/import_pb2.pyi b/test/generated_async_only/testproto/grpc/import_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/grpc/import_pb2.pyi rename to test/generated_async_only/testproto/grpc/import_pb2.pyi diff --git a/test/generated_async_only/testproto/grpc/import_pb2_grpc.pyi b/test/generated_async_only/testproto/grpc/import_pb2_grpc.pyi new file mode 100644 index 00000000..dcc2f2ef --- /dev/null +++ b/test/generated_async_only/testproto/grpc/import_pb2_grpc.pyi @@ -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 +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: ... diff --git a/test/generated-concrete/google/protobuf/duration_pb2.pyi b/test/generated_concrete/google/protobuf/duration_pb2.pyi similarity index 100% rename from test/generated-concrete/google/protobuf/duration_pb2.pyi rename to test/generated_concrete/google/protobuf/duration_pb2.pyi diff --git a/test/generated-concrete/mypy_protobuf/extensions_pb2.pyi b/test/generated_concrete/mypy_protobuf/extensions_pb2.pyi similarity index 100% rename from test/generated-concrete/mypy_protobuf/extensions_pb2.pyi rename to test/generated_concrete/mypy_protobuf/extensions_pb2.pyi diff --git a/test/generated-concrete/testproto/Capitalized/Capitalized_pb2.pyi b/test/generated_concrete/testproto/Capitalized/Capitalized_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/Capitalized/Capitalized_pb2.pyi rename to test/generated_concrete/testproto/Capitalized/Capitalized_pb2.pyi diff --git a/test/generated-concrete/testproto/comment_special_chars_pb2.pyi b/test/generated_concrete/testproto/comment_special_chars_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/comment_special_chars_pb2.pyi rename to test/generated_concrete/testproto/comment_special_chars_pb2.pyi diff --git a/test/generated-concrete/testproto/dot/com/test_pb2.pyi b/test/generated_concrete/testproto/dot/com/test_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/dot/com/test_pb2.pyi rename to test/generated_concrete/testproto/dot/com/test_pb2.pyi diff --git a/test/generated_concrete/testproto/edition2024_pb2.pyi b/test/generated_concrete/testproto/edition2024_pb2.pyi new file mode 100644 index 00000000..6de2ba64 --- /dev/null +++ b/test/generated_concrete/testproto/edition2024_pb2.pyi @@ -0,0 +1,68 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +Edition version of proto2 file""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing.final +class Editions2024SubMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + THING_FIELD_NUMBER: builtins.int + thing: builtins.str + def __init__( + self, + *, + 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: ... + +Global___Editions2024SubMessage: typing_extensions.TypeAlias = Editions2024SubMessage + +@typing.final +class Editions2024Test(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LEGACY_FIELD_NUMBER: builtins.int + EXPLICIT_SINGULAR_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_FIELD_NUMBER: builtins.int + IMPLICIT_SINGULAR_FIELD_NUMBER: builtins.int + DEFAULT_SINGULAR_FIELD_NUMBER: builtins.int + legacy: builtins.str + """Expect to be always set""" + explicit_singular: builtins.str + """Expect HasField generated""" + implicit_singular: builtins.str + """Expect implicit field presence, no HasField generated""" + default_singular: builtins.str + """Not set, should default to EXPLICIT""" + @property + def message_field(self) -> Global___Editions2024SubMessage: + """Expect HasField generated?""" + + def __init__( + self, + *, + legacy: builtins.str | None = ..., + explicit_singular: builtins.str | None = ..., + message_field: Global___Editions2024SubMessage | None = ..., + 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: ... + +Global___Editions2024Test: typing_extensions.TypeAlias = Editions2024Test diff --git a/test/generated_concrete/testproto/grpc/dummy_pb2.pyi b/test/generated_concrete/testproto/grpc/dummy_pb2.pyi new file mode 100644 index 00000000..ce03f2a5 --- /dev/null +++ b/test/generated_concrete/testproto/grpc/dummy_pb2.pyi @@ -0,0 +1,68 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +https://github.com/vmagamedov/grpclib/blob/master/tests/dummy.proto""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys +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 + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing.final +class DummyRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_FIELD_NUMBER: builtins.int + value: builtins.str + def __init__( + self, + *, + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ... + +Global___DummyRequest: typing_extensions.TypeAlias = DummyRequest + +@typing.final +class DummyReply(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_FIELD_NUMBER: builtins.int + value: builtins.str + def __init__( + self, + *, + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ... + +Global___DummyReply: typing_extensions.TypeAlias = DummyReply + +@deprecated("""This message has been marked as deprecated using proto message options.""") +@typing.final +class DeprecatedRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + OLD_FIELD_FIELD_NUMBER: builtins.int + old_field: builtins.str + def __init__( + self, + *, + old_field: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["old_field", b"old_field"]) -> None: ... + +Global___DeprecatedRequest: typing_extensions.TypeAlias = DeprecatedRequest diff --git a/test/generated-concrete/testproto/grpc/dummy_pb2_grpc.pyi b/test/generated_concrete/testproto/grpc/dummy_pb2_grpc.pyi similarity index 100% rename from test/generated-concrete/testproto/grpc/dummy_pb2_grpc.pyi rename to test/generated_concrete/testproto/grpc/dummy_pb2_grpc.pyi diff --git a/test/generated_concrete/testproto/grpc/import_pb2.pyi b/test/generated_concrete/testproto/grpc/import_pb2.pyi new file mode 100644 index 00000000..dd854e28 --- /dev/null +++ b/test/generated_concrete/testproto/grpc/import_pb2.pyi @@ -0,0 +1,8 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import google.protobuf.descriptor + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor diff --git a/test/generated-concrete/testproto/grpc/import_pb2_grpc.pyi b/test/generated_concrete/testproto/grpc/import_pb2_grpc.pyi similarity index 100% rename from test/generated-concrete/testproto/grpc/import_pb2_grpc.pyi rename to test/generated_concrete/testproto/grpc/import_pb2_grpc.pyi diff --git a/test/generated-concrete/testproto/inner/inner_pb2.pyi b/test/generated_concrete/testproto/inner/inner_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/inner/inner_pb2.pyi rename to test/generated_concrete/testproto/inner/inner_pb2.pyi diff --git a/test/generated-concrete/testproto/nested/nested_pb2.pyi b/test/generated_concrete/testproto/nested/nested_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/nested/nested_pb2.pyi rename to test/generated_concrete/testproto/nested/nested_pb2.pyi diff --git a/test/generated-concrete/testproto/nopackage_pb2.pyi b/test/generated_concrete/testproto/nopackage_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/nopackage_pb2.pyi rename to test/generated_concrete/testproto/nopackage_pb2.pyi diff --git a/test/generated-concrete/testproto/readme_enum_pb2.pyi b/test/generated_concrete/testproto/readme_enum_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/readme_enum_pb2.pyi rename to test/generated_concrete/testproto/readme_enum_pb2.pyi diff --git a/test/generated-concrete/testproto/reexport_pb2.pyi b/test/generated_concrete/testproto/reexport_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/reexport_pb2.pyi rename to test/generated_concrete/testproto/reexport_pb2.pyi diff --git a/test/generated-concrete/testproto/test3_pb2.pyi b/test/generated_concrete/testproto/test3_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/test3_pb2.pyi rename to test/generated_concrete/testproto/test3_pb2.pyi diff --git a/test/generated-concrete/testproto/test_extensions2_pb2.pyi b/test/generated_concrete/testproto/test_extensions2_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/test_extensions2_pb2.pyi rename to test/generated_concrete/testproto/test_extensions2_pb2.pyi diff --git a/test/generated-concrete/testproto/test_extensions3_pb2.pyi b/test/generated_concrete/testproto/test_extensions3_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/test_extensions3_pb2.pyi rename to test/generated_concrete/testproto/test_extensions3_pb2.pyi diff --git a/test/generated-concrete/testproto/test_no_generic_services_pb2.pyi b/test/generated_concrete/testproto/test_no_generic_services_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/test_no_generic_services_pb2.pyi rename to test/generated_concrete/testproto/test_no_generic_services_pb2.pyi diff --git a/test/generated-concrete/testproto/test_pb2.pyi b/test/generated_concrete/testproto/test_pb2.pyi similarity index 100% rename from test/generated-concrete/testproto/test_pb2.pyi rename to test/generated_concrete/testproto/test_pb2.pyi diff --git a/test/generated_sync_only/testproto/__init__.py b/test/generated_sync_only/testproto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/generated_sync_only/testproto/grpc/__init__.py b/test/generated_sync_only/testproto/grpc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/generated_sync_only/testproto/grpc/dummy_pb2.pyi b/test/generated_sync_only/testproto/grpc/dummy_pb2.pyi new file mode 100644 index 00000000..ce03f2a5 --- /dev/null +++ b/test/generated_sync_only/testproto/grpc/dummy_pb2.pyi @@ -0,0 +1,68 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +https://github.com/vmagamedov/grpclib/blob/master/tests/dummy.proto""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys +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 + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing.final +class DummyRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_FIELD_NUMBER: builtins.int + value: builtins.str + def __init__( + self, + *, + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ... + +Global___DummyRequest: typing_extensions.TypeAlias = DummyRequest + +@typing.final +class DummyReply(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_FIELD_NUMBER: builtins.int + value: builtins.str + def __init__( + self, + *, + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ... + +Global___DummyReply: typing_extensions.TypeAlias = DummyReply + +@deprecated("""This message has been marked as deprecated using proto message options.""") +@typing.final +class DeprecatedRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + OLD_FIELD_FIELD_NUMBER: builtins.int + old_field: builtins.str + def __init__( + self, + *, + old_field: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["old_field", b"old_field"]) -> None: ... + +Global___DeprecatedRequest: typing_extensions.TypeAlias = DeprecatedRequest diff --git a/test/generated_sync_only/testproto/grpc/dummy_pb2_grpc.pyi b/test/generated_sync_only/testproto/grpc/dummy_pb2_grpc.pyi new file mode 100644 index 00000000..3af5ac96 --- /dev/null +++ b/test/generated_sync_only/testproto/grpc/dummy_pb2_grpc.pyi @@ -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 +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.UnaryUnaryMultiCallable[ + testproto.grpc.dummy_pb2.DummyRequest, + testproto.grpc.dummy_pb2.DummyReply, +] +_DummyServiceUnaryStreamType: typing_extensions.TypeAlias = grpc.UnaryStreamMultiCallable[ + testproto.grpc.dummy_pb2.DummyRequest, + testproto.grpc.dummy_pb2.DummyReply, +] +_DummyServiceStreamUnaryType: typing_extensions.TypeAlias = grpc.StreamUnaryMultiCallable[ + testproto.grpc.dummy_pb2.DummyRequest, + testproto.grpc.dummy_pb2.DummyReply, +] +_DummyServiceStreamStreamType: typing_extensions.TypeAlias = grpc.StreamStreamMultiCallable[ + testproto.grpc.dummy_pb2.DummyRequest, + testproto.grpc.dummy_pb2.DummyReply, +] +class DummyServiceStub: + """DummyService""" + + def __init__(self, channel: grpc.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.ServicerContext, + ) -> testproto.grpc.dummy_pb2.DummyReply: + """UnaryUnary""" + + @abc.abstractmethod + def UnaryStream( + self, + request: testproto.grpc.dummy_pb2.DummyRequest, + context: grpc.ServicerContext, + ) -> collections.abc.Iterator[testproto.grpc.dummy_pb2.DummyReply]: + """UnaryStream""" + + @abc.abstractmethod + def StreamUnary( + self, + request_iterator: collections.abc.Iterator[testproto.grpc.dummy_pb2.DummyRequest], + context: grpc.ServicerContext, + ) -> testproto.grpc.dummy_pb2.DummyReply: + """StreamUnary""" + + @abc.abstractmethod + def StreamStream( + self, + request_iterator: collections.abc.Iterator[testproto.grpc.dummy_pb2.DummyRequest], + context: grpc.ServicerContext, + ) -> collections.abc.Iterator[testproto.grpc.dummy_pb2.DummyReply]: + """StreamStream""" + +def add_DummyServiceServicer_to_server(servicer: DummyServiceServicer, server: grpc.Server) -> None: ... + +_DeprecatedServiceDeprecatedMethodType: typing_extensions.TypeAlias = grpc.UnaryUnaryMultiCallable[ + testproto.grpc.dummy_pb2.DeprecatedRequest, + testproto.grpc.dummy_pb2.DummyReply, +] +_DeprecatedServiceDeprecatedMethodNotDeprecatedRequestType: typing_extensions.TypeAlias = grpc.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.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.ServicerContext, + ) -> testproto.grpc.dummy_pb2.DummyReply: + """DeprecatedMethod""" + + @abc.abstractmethod + def DeprecatedMethodNotDeprecatedRequest( + self, + request: testproto.grpc.dummy_pb2.DummyRequest, + context: grpc.ServicerContext, + ) -> testproto.grpc.dummy_pb2.DummyReply: + """DeprecatedMethodNotDeprecatedRequest""" + +@deprecated("""This service is deprecated""") +def add_DeprecatedServiceServicer_to_server(servicer: DeprecatedServiceServicer, server: grpc.Server) -> None: ... diff --git a/test/generated_sync_only/testproto/grpc/import_pb2.pyi b/test/generated_sync_only/testproto/grpc/import_pb2.pyi new file mode 100644 index 00000000..dd854e28 --- /dev/null +++ b/test/generated_sync_only/testproto/grpc/import_pb2.pyi @@ -0,0 +1,8 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import google.protobuf.descriptor + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor diff --git a/test/generated_sync_only/testproto/grpc/import_pb2_grpc.pyi b/test/generated_sync_only/testproto/grpc/import_pb2_grpc.pyi new file mode 100644 index 00000000..f0fe54df --- /dev/null +++ b/test/generated_sync_only/testproto/grpc/import_pb2_grpc.pyi @@ -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 +import sys +import testproto.test_pb2 +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.UnaryUnaryMultiCallable[ + google.protobuf.empty_pb2.Empty, + testproto.test_pb2.Simple1, +] +_SimpleServiceUnaryStreamType: typing_extensions.TypeAlias = grpc.UnaryUnaryMultiCallable[ + testproto.test_pb2.Simple1, + google.protobuf.empty_pb2.Empty, +] +_SimpleServiceNoCommentType: typing_extensions.TypeAlias = grpc.UnaryUnaryMultiCallable[ + testproto.test_pb2.Simple1, + google.protobuf.empty_pb2.Empty, +] +class SimpleServiceStub: + """SimpleService""" + + def __init__(self, channel: grpc.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.ServicerContext, + ) -> testproto.test_pb2.Simple1: + """UnaryUnary""" + + @abc.abstractmethod + def UnaryStream( + self, + request: testproto.test_pb2.Simple1, + context: grpc.ServicerContext, + ) -> google.protobuf.empty_pb2.Empty: + """UnaryStream""" + + @abc.abstractmethod + def NoComment( + self, + request: testproto.test_pb2.Simple1, + context: grpc.ServicerContext, + ) -> google.protobuf.empty_pb2.Empty: ... + +def add_SimpleServiceServicer_to_server(servicer: SimpleServiceServicer, server: grpc.Server) -> None: ... diff --git a/test/sync_only/test_sync_only.py b/test/sync_only/test_sync_only.py new file mode 100644 index 00000000..99597587 --- /dev/null +++ b/test/sync_only/test_sync_only.py @@ -0,0 +1,27 @@ +""" +Type-checking test for sync_only GRPC stubs. + +This module is run through mypy to validate that stubs generated with the +only_sync flag have the correct types: +- Regular (non-generic) Stub class that only accepts grpc.Channel +- Servicer methods use Iterator for client streaming (not _MaybeAsyncIterator) +- add_XXXServicer_to_server accepts grpc.Server +""" + +import grpc +from testproto.grpc import dummy_pb2, dummy_pb2_grpc + + +class AsyncOnlyServicer(dummy_pb2_grpc.DummyServiceServicer): + def UnaryUnary( + self, + request: dummy_pb2.DummyRequest, + context: grpc.ServicerContext, + ) -> dummy_pb2.DummyReply: + return dummy_pb2.DummyReply(value=request.value[::-1]) + + +def noop() -> None: + """Don't actually run anything; this is just for type-checking.""" + stub = dummy_pb2_grpc.DummyServiceStub(channel=grpc.insecure_channel("localhost:50051")) + stub.UnaryUnary(dummy_pb2.DummyRequest(value="test"))