diff --git a/src/python/grpcio/grpc/__init__.py b/src/python/grpcio/grpc/__init__.py index 2a177be02fc..a6dd59fc13a 100644 --- a/src/python/grpcio/grpc/__init__.py +++ b/src/python/grpcio/grpc/__init__.py @@ -1982,11 +1982,13 @@ def _default_get_protos_and_services(*args, **kwargs): try: import grpc_tools except ImportError: - get_protos = _default_get_protos - get_services = _default_get_services - get_protos_and_services = _default_get_protos_and_services + protos = _default_get_protos + services = _default_get_services + protos_and_services = _default_get_protos_and_services else: - from grpc_tools.protoc import get_protos, get_services, get_protos_and_services + from grpc_tools.protoc import _protos as protos + from grpc_tools.protoc import _services as services + from grpc_tools.protoc import _protos_and_services as protos_and_services ################################### __all__ ################################# @@ -2046,9 +2048,9 @@ __all__ = ( 'secure_channel', 'intercept_channel', 'server', - 'get_protos', - 'get_services', - 'get_protos_and_services', + 'protos', + 'services', + 'protos_and_services', ) ############################### Extension Shims ################################ diff --git a/src/python/grpcio_tests/tests/unit/_dynamic_stubs_test.py b/src/python/grpcio_tests/tests/unit/_dynamic_stubs_test.py new file mode 100644 index 00000000000..2c70e44cf1e --- /dev/null +++ b/src/python/grpcio_tests/tests/unit/_dynamic_stubs_test.py @@ -0,0 +1,87 @@ +# Copyright 2019 The gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test of dynamic stub import API.""" + +import unittest +import logging +import contextlib +import sys +import multiprocessing +import functools + +# TODO: Support setup.py as test runner. +# TODO: Support Blaze as test runner. + +@contextlib.contextmanager +def _grpc_tools_unimportable(): + original_sys_path = sys.path + sys.path = [path for path in sys.path if "grpcio_tools" not in path] + try: + yield + finally: + sys.path = original_sys_path + + +# TODO: Dedupe with grpc_tools test? +def _wrap_in_subprocess(error_queue, fn): + @functools.wraps(fn) + def _wrapped(): + try: + fn() + except Exception as e: + error_queue.put(e) + raise + return _wrapped + + +def _run_in_subprocess(test_case): + error_queue = multiprocessing.Queue() + wrapped_case = _wrap_in_subprocess(error_queue, test_case) + proc = multiprocessing.Process(target=wrapped_case) + proc.start() + proc.join() + if not error_queue.empty(): + raise error_queue.get() + assert proc.exitcode == 0, "Process exited with code {}".format(proc.exitcode) + + +def _test_sunny_day(): + import grpc + protos, services = grpc.protos_and_services("tests/unit/data/foo/bar.proto") + assert protos.BarMessage is not None + assert services.BarStub is not None + + +def _test_grpc_tools_unimportable(): + with _grpc_tools_unimportable(): + import grpc + try: + protos, services = grpc.protos_and_services("tests/unit/data/foo/bar.proto") + except NotImplementedError as e: + assert "grpcio-tools" in str(e) + else: + assert False, "Did not raise NotImplementedError" + + +class DynamicStubTest(unittest.TestCase): + + def test_sunny_day(self): + _run_in_subprocess(_test_sunny_day) + + def test_grpc_tools_unimportable(self): + _run_in_subprocess(_test_grpc_tools_unimportable) + +if __name__ == "__main__": + logging.basicConfig() + unittest.main(verbosity=2) diff --git a/src/python/grpcio_tests/tests/unit/data/foo/bar.proto b/src/python/grpcio_tests/tests/unit/data/foo/bar.proto new file mode 100644 index 00000000000..11d0dfbe782 --- /dev/null +++ b/src/python/grpcio_tests/tests/unit/data/foo/bar.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package tests.unit.data.foo.bar; + +message BarMessage { + string a = 1; +}; + +service Bar { + rpc GetBar(BarMessage) returns (BarMessage); +}; diff --git a/tools/distrib/python/grpcio_tools/grpc_tools/protoc.py b/tools/distrib/python/grpcio_tools/grpc_tools/protoc.py index 9c9eda52281..8f78dbb991a 100644 --- a/tools/distrib/python/grpcio_tools/grpc_tools/protoc.py +++ b/tools/distrib/python/grpcio_tools/grpc_tools/protoc.py @@ -65,24 +65,24 @@ def _augmented_syspath(new_paths): # TODO: Investigate making this even more of a no-op in the case that we have # truly already imported the module. -def get_protos(protobuf_path, include_paths=None): +def _protos(protobuf_path, include_paths=None): with _augmented_syspath(include_paths): module_name = _proto_file_to_module_name(_PROTO_MODULE_SUFFIX, protobuf_path) module = importlib.import_module(module_name) return module -def get_services(protobuf_path, include_paths=None): - get_protos(protobuf_path, include_paths) +def _services(protobuf_path, include_paths=None): + _protos(protobuf_path, include_paths) with _augmented_syspath(include_paths): module_name = _proto_file_to_module_name(_SERVICE_MODULE_SUFFIX, protobuf_path) module = importlib.import_module(module_name) return module -def get_protos_and_services(protobuf_path, include_paths=None): - return (get_protos(protobuf_path, include_paths=include_paths), - get_services(protobuf_path, include_paths=include_paths)) +def _protos_and_services(protobuf_path, include_paths=None): + return (_protos(protobuf_path, include_paths=include_paths), + _services(protobuf_path, include_paths=include_paths)) _proto_code_cache = {} diff --git a/tools/distrib/python/grpcio_tools/grpc_tools/protoc_test.py b/tools/distrib/python/grpcio_tools/grpc_tools/protoc_test.py index d47c3ff151b..f41ad6fadf0 100644 --- a/tools/distrib/python/grpcio_tools/grpc_tools/protoc_test.py +++ b/tools/distrib/python/grpcio_tools/grpc_tools/protoc_test.py @@ -32,38 +32,38 @@ def _run_in_subprocess(test_case): def _test_import_protos(): from grpc_tools import protoc proto_path = "tools/distrib/python/grpcio_tools/" - protos = protoc.get_protos("grpc_tools/simple.proto", [proto_path]) + protos = protoc._protos("grpc_tools/simple.proto", [proto_path]) assert protos.SimpleMessage is not None def _test_import_services(): from grpc_tools import protoc proto_path = "tools/distrib/python/grpcio_tools/" - protos = protoc.get_protos("grpc_tools/simple.proto", [proto_path]) - services = protoc.get_services("grpc_tools/simple.proto", [proto_path]) + protos = protoc._protos("grpc_tools/simple.proto", [proto_path]) + services = protoc._services("grpc_tools/simple.proto", [proto_path]) assert services.SimpleMessageServiceStub is not None # NOTE: In this case, we use sys.path to determine where to look for our protos. def _test_import_implicit_include(): from grpc_tools import protoc - protos = protoc.get_protos("grpc_tools/simple.proto") - services = protoc.get_services("grpc_tools/simple.proto") + protos = protoc._protos("grpc_tools/simple.proto") + services = protoc._services("grpc_tools/simple.proto") assert services.SimpleMessageServiceStub is not None def _test_import_services_without_protos(): from grpc_tools import protoc - services = protoc.get_services("grpc_tools/simple.proto") + services = protoc._services("grpc_tools/simple.proto") assert services.SimpleMessageServiceStub is not None def _test_proto_module_imported_once(): from grpc_tools import protoc proto_path = "tools/distrib/python/grpcio_tools/" - protos = protoc.get_protos("grpc_tools/simple.proto", [proto_path]) - services = protoc.get_services("grpc_tools/simple.proto", [proto_path]) - complicated_protos = protoc.get_protos("grpc_tools/complicated.proto", [proto_path]) + protos = protoc._protos("grpc_tools/simple.proto", [proto_path]) + services = protoc._services("grpc_tools/simple.proto", [proto_path]) + complicated_protos = protoc._protos("grpc_tools/complicated.proto", [proto_path]) assert (complicated_protos.grpc__tools_dot_simplest__pb2.SimplestMessage is protos.grpc__tools_dot_simpler__pb2.grpc__tools_dot_simplest__pb2.SimplestMessage) @@ -72,14 +72,14 @@ def _test_static_dynamic_combo(): from grpc_tools import complicated_pb2 from grpc_tools import protoc proto_path = "tools/distrib/python/grpcio_tools/" - protos = protoc.get_protos("grpc_tools/simple.proto", [proto_path]) + protos = protoc._protos("grpc_tools/simple.proto", [proto_path]) assert (complicated_pb2.grpc__tools_dot_simplest__pb2.SimplestMessage is protos.grpc__tools_dot_simpler__pb2.grpc__tools_dot_simplest__pb2.SimplestMessage) def _test_combined_import(): from grpc_tools import protoc - protos, services = protoc.get_protos_and_services("grpc_tools/simple.proto") + protos, services = protoc._protos_and_services("grpc_tools/simple.proto") assert protos.SimpleMessage is not None assert services.SimpleMessageServiceStub is not None @@ -87,7 +87,7 @@ def _test_combined_import(): def _test_syntax_errors(): from grpc_tools import protoc try: - protos = protoc.get_protos("grpc_tools/flawed.proto") + protos = protoc._protos("grpc_tools/flawed.proto") except Exception as e: error_str = str(e) assert "flawed.proto" in error_str