From 45b3230ef2b6a4a7e4c7a2348fa65b2c59fe579a Mon Sep 17 00:00:00 2001 From: Lidi Zheng Date: Wed, 12 Dec 2018 17:16:12 -0800 Subject: [PATCH 1/5] Add grpcio-status extension package * The new package has 2 API `from_call` and `to_status` * Utilize the experimental API `abort_with_status` * Add 5 unit test cases --- requirements.bazel.txt | 1 + src/python/grpcio_status/.gitignore | 3 + src/python/grpcio_status/MANIFEST.in | 3 + src/python/grpcio_status/README.rst | 9 + .../grpcio_status/grpc_status/BUILD.bazel | 14 ++ .../grpcio_status/grpc_status/__init__.py | 13 ++ .../grpcio_status/grpc_status/rpc_status.py | 88 +++++++++ src/python/grpcio_status/grpc_version.py | 17 ++ src/python/grpcio_status/setup.py | 86 +++++++++ src/python/grpcio_tests/setup.py | 1 + .../grpcio_tests/tests/status/BUILD.bazel | 19 ++ .../grpcio_tests/tests/status/__init__.py | 13 ++ .../tests/status/_grpc_status_test.py | 175 ++++++++++++++++++ src/python/grpcio_tests/tests/tests.json | 1 + .../grpcio_status/grpc_version.py.template | 19 ++ tools/distrib/pylint_code.sh | 1 + .../artifacts/build_artifact_python.sh | 5 + .../run_tests/helper_scripts/build_python.sh | 8 +- 18 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 src/python/grpcio_status/.gitignore create mode 100644 src/python/grpcio_status/MANIFEST.in create mode 100644 src/python/grpcio_status/README.rst create mode 100644 src/python/grpcio_status/grpc_status/BUILD.bazel create mode 100644 src/python/grpcio_status/grpc_status/__init__.py create mode 100644 src/python/grpcio_status/grpc_status/rpc_status.py create mode 100644 src/python/grpcio_status/grpc_version.py create mode 100644 src/python/grpcio_status/setup.py create mode 100644 src/python/grpcio_tests/tests/status/BUILD.bazel create mode 100644 src/python/grpcio_tests/tests/status/__init__.py create mode 100644 src/python/grpcio_tests/tests/status/_grpc_status_test.py create mode 100644 templates/src/python/grpcio_status/grpc_version.py.template diff --git a/requirements.bazel.txt b/requirements.bazel.txt index 7794aec752e..e97843794e4 100644 --- a/requirements.bazel.txt +++ b/requirements.bazel.txt @@ -13,3 +13,4 @@ urllib3>=1.23 chardet==3.0.4 certifi==2017.4.17 idna==2.7 +googleapis-common-protos==1.5.5 diff --git a/src/python/grpcio_status/.gitignore b/src/python/grpcio_status/.gitignore new file mode 100644 index 00000000000..19d1523efde --- /dev/null +++ b/src/python/grpcio_status/.gitignore @@ -0,0 +1,3 @@ +build/ +grpcio_status.egg-info/ +dist/ diff --git a/src/python/grpcio_status/MANIFEST.in b/src/python/grpcio_status/MANIFEST.in new file mode 100644 index 00000000000..eca719a9c20 --- /dev/null +++ b/src/python/grpcio_status/MANIFEST.in @@ -0,0 +1,3 @@ +include grpc_version.py +recursive-include grpc_status *.py +global-exclude *.pyc diff --git a/src/python/grpcio_status/README.rst b/src/python/grpcio_status/README.rst new file mode 100644 index 00000000000..dc2f7b1dab1 --- /dev/null +++ b/src/python/grpcio_status/README.rst @@ -0,0 +1,9 @@ +gRPC Python Status Proto +=========================== + +Reference package for GRPC Python status proto mapping. + +Dependencies +------------ + +Depends on the `grpcio` package, available from PyPI via `pip install grpcio`. diff --git a/src/python/grpcio_status/grpc_status/BUILD.bazel b/src/python/grpcio_status/grpc_status/BUILD.bazel new file mode 100644 index 00000000000..223a077c3f2 --- /dev/null +++ b/src/python/grpcio_status/grpc_status/BUILD.bazel @@ -0,0 +1,14 @@ +load("@grpc_python_dependencies//:requirements.bzl", "requirement") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "grpc_status", + srcs = ["rpc_status.py",], + deps = [ + "//src/python/grpcio/grpc:grpcio", + requirement('protobuf'), + requirement('googleapis-common-protos'), + ], + imports=["../",], +) diff --git a/src/python/grpcio_status/grpc_status/__init__.py b/src/python/grpcio_status/grpc_status/__init__.py new file mode 100644 index 00000000000..38fdfc9c5cf --- /dev/null +++ b/src/python/grpcio_status/grpc_status/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 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. diff --git a/src/python/grpcio_status/grpc_status/rpc_status.py b/src/python/grpcio_status/grpc_status/rpc_status.py new file mode 100644 index 00000000000..36c8eba37d5 --- /dev/null +++ b/src/python/grpcio_status/grpc_status/rpc_status.py @@ -0,0 +1,88 @@ +# Copyright 2018 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. +"""Reference implementation for status mapping in gRPC Python.""" + +import collections + +import grpc + +# TODO(https://github.com/bazelbuild/bazel/issues/6844) +# Due to Bazel issue, the namespace packages won't resolve correctly. +# Adding this unused-import as a workaround to avoid module-not-found error +# under Bazel builds. +import google.protobuf # pylint: disable=unused-import +from google.rpc import status_pb2 + +_CODE_TO_GRPC_CODE_MAPPING = dict([(x.value[0], x) for x in grpc.StatusCode]) + +_GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin' + + +class _Status( + collections.namedtuple( + '_Status', ('code', 'details', 'trailing_metadata')), grpc.Status): + pass + + +def _code_to_grpc_status_code(code): + try: + return _CODE_TO_GRPC_CODE_MAPPING[code] + except KeyError: + raise ValueError('Invalid status code %s' % code) + + +def from_call(call): + """Returns a google.rpc.status.Status message corresponding to a given grpc.Call. + + Args: + call: A grpc.Call instance. + + Returns: + A google.rpc.status.Status message representing the status of the RPC. + + Raises: + ValueError: If the status code, status message is inconsistent with the rich status + inside of the google.rpc.status.Status. + """ + for key, value in call.trailing_metadata(): + if key == _GRPC_DETAILS_METADATA_KEY: + rich_status = status_pb2.Status.FromString(value) + if call.code().value[0] != rich_status.code: + raise ValueError( + 'Code in Status proto (%s) doesn\'t match status code (%s)' + % (_code_to_grpc_status_code(rich_status.code), + call.code())) + if call.details() != rich_status.message: + raise ValueError( + 'Message in Status proto (%s) doesn\'t match status details (%s)' + % (rich_status.message, call.details())) + return rich_status + return None + + +def to_status(status): + """Convert a google.rpc.status.Status message to grpc.Status. + + Args: + status: a google.rpc.status.Status message representing the non-OK status + to terminate the RPC with and communicate it to the client. + + Returns: + A grpc.Status instance. + """ + return _Status( + code=_code_to_grpc_status_code(status.code), + details=status.message, + trailing_metadata=((_GRPC_DETAILS_METADATA_KEY, + status.SerializeToString()),)) diff --git a/src/python/grpcio_status/grpc_version.py b/src/python/grpcio_status/grpc_version.py new file mode 100644 index 00000000000..e009843b94f --- /dev/null +++ b/src/python/grpcio_status/grpc_version.py @@ -0,0 +1,17 @@ +# Copyright 2018 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. + +# AUTO-GENERATED FROM `$REPO_ROOT/templates/src/python/grpcio_status/grpc_version.py.template`!!! + +VERSION = '1.18.0.dev0' diff --git a/src/python/grpcio_status/setup.py b/src/python/grpcio_status/setup.py new file mode 100644 index 00000000000..0601498bc51 --- /dev/null +++ b/src/python/grpcio_status/setup.py @@ -0,0 +1,86 @@ +# Copyright 2018 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. +"""Setup module for the GRPC Python package's status mapping.""" + +import os + +import setuptools + +# Ensure we're in the proper directory whether or not we're being used by pip. +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +# Break import-style to ensure we can actually find our local modules. +import grpc_version + + +class _NoOpCommand(setuptools.Command): + """No-op command.""" + + description = '' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + pass + + +CLASSIFIERS = [ + 'Development Status :: 5 - Production/Stable', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'License :: OSI Approved :: Apache Software License', +] + +PACKAGE_DIRECTORIES = { + '': '.', +} + +INSTALL_REQUIRES = ( + 'protobuf>=3.6.0', + 'grpcio>={version}'.format(version=grpc_version.VERSION), + 'googleapis-common-protos>=1.5.5', +) + +SETUP_REQUIRES = () +COMMAND_CLASS = { + # wire up commands to no-op not to break the external dependencies + 'preprocess': _NoOpCommand, + 'build_package_protos': _NoOpCommand, +} + +setuptools.setup( + name='grpcio-status', + version=grpc_version.VERSION, + description='Status proto mapping for gRPC', + author='The gRPC Authors', + author_email='grpc-io@googlegroups.com', + url='https://grpc.io', + license='Apache License 2.0', + classifiers=CLASSIFIERS, + package_dir=PACKAGE_DIRECTORIES, + packages=setuptools.find_packages('.'), + install_requires=INSTALL_REQUIRES, + setup_requires=SETUP_REQUIRES, + cmdclass=COMMAND_CLASS) diff --git a/src/python/grpcio_tests/setup.py b/src/python/grpcio_tests/setup.py index f56425ac6d3..f9cb9d0cec9 100644 --- a/src/python/grpcio_tests/setup.py +++ b/src/python/grpcio_tests/setup.py @@ -40,6 +40,7 @@ INSTALL_REQUIRES = ( 'coverage>=4.0', 'enum34>=1.0.4', 'grpcio>={version}'.format(version=grpc_version.VERSION), 'grpcio-channelz>={version}'.format(version=grpc_version.VERSION), + 'grpcio-status>={version}'.format(version=grpc_version.VERSION), 'grpcio-tools>={version}'.format(version=grpc_version.VERSION), 'grpcio-health-checking>={version}'.format(version=grpc_version.VERSION), 'oauth2client>=1.4.7', 'protobuf>=3.6.0', 'six>=1.10', 'google-auth>=1.0.0', diff --git a/src/python/grpcio_tests/tests/status/BUILD.bazel b/src/python/grpcio_tests/tests/status/BUILD.bazel new file mode 100644 index 00000000000..937e50498e0 --- /dev/null +++ b/src/python/grpcio_tests/tests/status/BUILD.bazel @@ -0,0 +1,19 @@ +load("@grpc_python_dependencies//:requirements.bzl", "requirement") + +package(default_visibility = ["//visibility:public"]) + +py_test( + name = "grpc_status_test", + srcs = ["_grpc_status_test.py"], + main = "_grpc_status_test.py", + size = "small", + deps = [ + "//src/python/grpcio/grpc:grpcio", + "//src/python/grpcio_status/grpc_status:grpc_status", + "//src/python/grpcio_tests/tests/unit:test_common", + "//src/python/grpcio_tests/tests/unit/framework/common:common", + requirement('protobuf'), + requirement('googleapis-common-protos'), + ], + imports = ["../../",], +) diff --git a/src/python/grpcio_tests/tests/status/__init__.py b/src/python/grpcio_tests/tests/status/__init__.py new file mode 100644 index 00000000000..38fdfc9c5cf --- /dev/null +++ b/src/python/grpcio_tests/tests/status/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 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. diff --git a/src/python/grpcio_tests/tests/status/_grpc_status_test.py b/src/python/grpcio_tests/tests/status/_grpc_status_test.py new file mode 100644 index 00000000000..5969338736f --- /dev/null +++ b/src/python/grpcio_tests/tests/status/_grpc_status_test.py @@ -0,0 +1,175 @@ +# Copyright 2018 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. +"""Tests of grpc_status.""" + +import unittest + +import logging +import traceback + +import grpc +from grpc_status import rpc_status + +from tests.unit import test_common + +from google.protobuf import any_pb2 +from google.rpc import code_pb2, status_pb2, error_details_pb2 + +_STATUS_OK = '/test/StatusOK' +_STATUS_NOT_OK = '/test/StatusNotOk' +_ERROR_DETAILS = '/test/ErrorDetails' +_INCONSISTENT = '/test/Inconsistent' +_INVALID_CODE = '/test/InvalidCode' + +_REQUEST = b'\x00\x00\x00' +_RESPONSE = b'\x01\x01\x01' + +_GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin' + +_STATUS_DETAILS = 'This is an error detail' +_STATUS_DETAILS_ANOTHER = 'This is another error detail' + + +def _ok_unary_unary(request, servicer_context): + return _RESPONSE + + +def _not_ok_unary_unary(request, servicer_context): + servicer_context.abort(grpc.StatusCode.INTERNAL, _STATUS_DETAILS) + + +def _error_details_unary_unary(request, servicer_context): + details = any_pb2.Any() + details.Pack( + error_details_pb2.DebugInfo( + stack_entries=traceback.format_stack(), + detail='Intensionally invoked')) + rich_status = status_pb2.Status( + code=code_pb2.INTERNAL, + message=_STATUS_DETAILS, + details=[details], + ) + servicer_context.abort_with_status(rpc_status.to_status(rich_status)) + + +def _inconsistent_unary_unary(request, servicer_context): + rich_status = status_pb2.Status( + code=code_pb2.INTERNAL, + message=_STATUS_DETAILS, + ) + servicer_context.set_code(grpc.StatusCode.NOT_FOUND) + servicer_context.set_details(_STATUS_DETAILS_ANOTHER) + # User put inconsistent status information in trailing metadata + servicer_context.set_trailing_metadata(((_GRPC_DETAILS_METADATA_KEY, + rich_status.SerializeToString()),)) + + +def _invalid_code_unary_unary(request, servicer_context): + rich_status = status_pb2.Status( + code=42, + message='Invalid code', + ) + servicer_context.abort_with_status(rpc_status.to_status(rich_status)) + + +class _GenericHandler(grpc.GenericRpcHandler): + + def service(self, handler_call_details): + if handler_call_details.method == _STATUS_OK: + return grpc.unary_unary_rpc_method_handler(_ok_unary_unary) + elif handler_call_details.method == _STATUS_NOT_OK: + return grpc.unary_unary_rpc_method_handler(_not_ok_unary_unary) + elif handler_call_details.method == _ERROR_DETAILS: + return grpc.unary_unary_rpc_method_handler( + _error_details_unary_unary) + elif handler_call_details.method == _INCONSISTENT: + return grpc.unary_unary_rpc_method_handler( + _inconsistent_unary_unary) + elif handler_call_details.method == _INVALID_CODE: + return grpc.unary_unary_rpc_method_handler( + _invalid_code_unary_unary) + else: + return None + + +class StatusTest(unittest.TestCase): + + def setUp(self): + self._server = test_common.test_server() + self._server.add_generic_rpc_handlers((_GenericHandler(),)) + port = self._server.add_insecure_port('[::]:0') + self._server.start() + + self._channel = grpc.insecure_channel('localhost:%d' % port) + + def tearDown(self): + self._server.stop(None) + self._channel.close() + + def test_status_ok(self): + try: + _, call = self._channel.unary_unary(_STATUS_OK).with_call(_REQUEST) + except grpc.RpcError as rpc_error: + self.fail(rpc_error) + # Succeed RPC doesn't have status + status = rpc_status.from_call(call) + self.assertIs(status, None) + + def test_status_not_ok(self): + with self.assertRaises(grpc.RpcError) as exception_context: + self._channel.unary_unary(_STATUS_NOT_OK).with_call(_REQUEST) + rpc_error = exception_context.exception + + self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL) + # Failed RPC doesn't automatically generate status + status = rpc_status.from_call(rpc_error) + self.assertIs(status, None) + + def test_error_details(self): + with self.assertRaises(grpc.RpcError) as exception_context: + self._channel.unary_unary(_ERROR_DETAILS).with_call(_REQUEST) + rpc_error = exception_context.exception + + status = rpc_status.from_call(rpc_error) + self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL) + self.assertEqual(status.code, code_pb2.Code.Value('INTERNAL')) + + # Check if the underlying proto message is intact + self.assertEqual(status.details[0].Is( + error_details_pb2.DebugInfo.DESCRIPTOR), True) + info = error_details_pb2.DebugInfo() + status.details[0].Unpack(info) + self.assertIn('_error_details_unary_unary', info.stack_entries[-1]) + + def test_code_message_validation(self): + with self.assertRaises(grpc.RpcError) as exception_context: + self._channel.unary_unary(_INCONSISTENT).with_call(_REQUEST) + rpc_error = exception_context.exception + self.assertEqual(rpc_error.code(), grpc.StatusCode.NOT_FOUND) + + # Code/Message validation failed + self.assertRaises(ValueError, rpc_status.from_call, rpc_error) + + def test_invalid_code(self): + with self.assertRaises(grpc.RpcError) as exception_context: + self._channel.unary_unary(_INVALID_CODE).with_call(_REQUEST) + rpc_error = exception_context.exception + self.assertEqual(rpc_error.code(), grpc.StatusCode.UNKNOWN) + # Invalid status code exception raised during coversion + self.assertIn('Invalid status code', rpc_error.details()) + + +if __name__ == '__main__': + logging.basicConfig() + unittest.main(verbosity=2) diff --git a/src/python/grpcio_tests/tests/tests.json b/src/python/grpcio_tests/tests/tests.json index 44700e979e9..b27e6f26938 100644 --- a/src/python/grpcio_tests/tests/tests.json +++ b/src/python/grpcio_tests/tests/tests.json @@ -15,6 +15,7 @@ "protoc_plugin._split_definitions_test.SplitProtoSingleProtocExecutionProtocStyleTest", "protoc_plugin.beta_python_plugin_test.PythonPluginTest", "reflection._reflection_servicer_test.ReflectionServicerTest", + "status._grpc_status_test.StatusTest", "testing._client_test.ClientTest", "testing._server_test.FirstServiceServicerTest", "testing._time_test.StrictFakeTimeTest", diff --git a/templates/src/python/grpcio_status/grpc_version.py.template b/templates/src/python/grpcio_status/grpc_version.py.template new file mode 100644 index 00000000000..727e01edb93 --- /dev/null +++ b/templates/src/python/grpcio_status/grpc_version.py.template @@ -0,0 +1,19 @@ +%YAML 1.2 +--- | + # Copyright 2018 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. + + # AUTO-GENERATED FROM `$REPO_ROOT/templates/src/python/grpcio_status/grpc_version.py.template`!!! + + VERSION = '${settings.python_version.pep440()}' diff --git a/tools/distrib/pylint_code.sh b/tools/distrib/pylint_code.sh index d17eb9fdb82..8a5f7af6c6c 100755 --- a/tools/distrib/pylint_code.sh +++ b/tools/distrib/pylint_code.sh @@ -24,6 +24,7 @@ DIRS=( 'src/python/grpcio_health_checking/grpc_health' 'src/python/grpcio_reflection/grpc_reflection' 'src/python/grpcio_testing/grpc_testing' + 'src/python/grpcio_status/grpc_status' ) TEST_DIRS=( diff --git a/tools/run_tests/artifacts/build_artifact_python.sh b/tools/run_tests/artifacts/build_artifact_python.sh index 605470325ab..18eb70c8574 100755 --- a/tools/run_tests/artifacts/build_artifact_python.sh +++ b/tools/run_tests/artifacts/build_artifact_python.sh @@ -123,6 +123,11 @@ then ${SETARCH_CMD} "${PYTHON}" src/python/grpcio_reflection/setup.py \ preprocess build_package_protos sdist cp -r src/python/grpcio_reflection/dist/* "$ARTIFACT_DIR" + + # Build grpcio_status source distribution + ${SETARCH_CMD} "${PYTHON}" src/python/grpcio_status/setup.py \ + preprocess build_package_protos sdist + cp -r src/python/grpcio_status/dist/* "$ARTIFACT_DIR" fi cp -r dist/* "$ARTIFACT_DIR" diff --git a/tools/run_tests/helper_scripts/build_python.sh b/tools/run_tests/helper_scripts/build_python.sh index e43f1fb429c..7cd1ef9d517 100755 --- a/tools/run_tests/helper_scripts/build_python.sh +++ b/tools/run_tests/helper_scripts/build_python.sh @@ -204,12 +204,18 @@ $VENV_PYTHON "$ROOT/src/python/grpcio_reflection/setup.py" preprocess $VENV_PYTHON "$ROOT/src/python/grpcio_reflection/setup.py" build_package_protos pip_install_dir "$ROOT/src/python/grpcio_reflection" +# Build/install status proto mapping +$VENV_PYTHON "$ROOT/src/python/grpcio_status/setup.py" preprocess +$VENV_PYTHON "$ROOT/src/python/grpcio_status/setup.py" build_package_protos +pip_install_dir "$ROOT/src/python/grpcio_status" + # Install testing pip_install_dir "$ROOT/src/python/grpcio_testing" # Build/install tests $VENV_PYTHON -m pip install coverage==4.4 oauth2client==4.1.0 \ - google-auth==1.0.0 requests==2.14.2 + google-auth==1.0.0 requests==2.14.2 \ + googleapis-common-protos==1.5.5 $VENV_PYTHON "$ROOT/src/python/grpcio_tests/setup.py" preprocess $VENV_PYTHON "$ROOT/src/python/grpcio_tests/setup.py" build_package_protos pip_install_dir "$ROOT/src/python/grpcio_tests" From 0e2f4061f5cc035b61fe7ee22db38529735fb618 Mon Sep 17 00:00:00 2001 From: Lidi Zheng Date: Thu, 13 Dec 2018 17:05:39 -0800 Subject: [PATCH 2/5] Fix typo && remove unecessary except --- src/python/grpcio_tests/tests/status/_grpc_status_test.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/python/grpcio_tests/tests/status/_grpc_status_test.py b/src/python/grpcio_tests/tests/status/_grpc_status_test.py index 5969338736f..519c372a960 100644 --- a/src/python/grpcio_tests/tests/status/_grpc_status_test.py +++ b/src/python/grpcio_tests/tests/status/_grpc_status_test.py @@ -54,7 +54,7 @@ def _error_details_unary_unary(request, servicer_context): details.Pack( error_details_pb2.DebugInfo( stack_entries=traceback.format_stack(), - detail='Intensionally invoked')) + detail='Intentionally invoked')) rich_status = status_pb2.Status( code=code_pb2.INTERNAL, message=_STATUS_DETAILS, @@ -118,10 +118,8 @@ class StatusTest(unittest.TestCase): self._channel.close() def test_status_ok(self): - try: - _, call = self._channel.unary_unary(_STATUS_OK).with_call(_REQUEST) - except grpc.RpcError as rpc_error: - self.fail(rpc_error) + _, call = self._channel.unary_unary(_STATUS_OK).with_call(_REQUEST) + # Succeed RPC doesn't have status status = rpc_status.from_call(call) self.assertIs(status, None) From b9cb2459ea4b2956547abf5fb516b553d85f1ae2 Mon Sep 17 00:00:00 2001 From: Lidi Zheng Date: Thu, 13 Dec 2018 17:18:38 -0800 Subject: [PATCH 3/5] Include LICENSE in artifact --- src/python/grpcio_status/MANIFEST.in | 1 + src/python/grpcio_status/setup.py | 19 +++++---- src/python/grpcio_status/status_commands.py | 39 +++++++++++++++++++ .../artifacts/build_artifact_python.sh | 2 +- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/python/grpcio_status/status_commands.py diff --git a/src/python/grpcio_status/MANIFEST.in b/src/python/grpcio_status/MANIFEST.in index eca719a9c20..09b8ea721e8 100644 --- a/src/python/grpcio_status/MANIFEST.in +++ b/src/python/grpcio_status/MANIFEST.in @@ -1,3 +1,4 @@ include grpc_version.py recursive-include grpc_status *.py global-exclude *.pyc +include LICENSE diff --git a/src/python/grpcio_status/setup.py b/src/python/grpcio_status/setup.py index 0601498bc51..a59cdd0f0f5 100644 --- a/src/python/grpcio_status/setup.py +++ b/src/python/grpcio_status/setup.py @@ -63,12 +63,18 @@ INSTALL_REQUIRES = ( 'googleapis-common-protos>=1.5.5', ) -SETUP_REQUIRES = () -COMMAND_CLASS = { - # wire up commands to no-op not to break the external dependencies - 'preprocess': _NoOpCommand, - 'build_package_protos': _NoOpCommand, -} +try: + import status_commands as _status_commands + # we are in the build environment, otherwise the above import fails + COMMAND_CLASS = { + # Run preprocess from the repository *before* doing any packaging! + 'preprocess': _status_commands.Preprocess, + } +except ImportError: + COMMAND_CLASS = { + # wire up commands to no-op not to break the external dependencies + 'preprocess': _NoOpCommand, + } setuptools.setup( name='grpcio-status', @@ -82,5 +88,4 @@ setuptools.setup( package_dir=PACKAGE_DIRECTORIES, packages=setuptools.find_packages('.'), install_requires=INSTALL_REQUIRES, - setup_requires=SETUP_REQUIRES, cmdclass=COMMAND_CLASS) diff --git a/src/python/grpcio_status/status_commands.py b/src/python/grpcio_status/status_commands.py new file mode 100644 index 00000000000..78cd497f622 --- /dev/null +++ b/src/python/grpcio_status/status_commands.py @@ -0,0 +1,39 @@ +# Copyright 2018 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. +"""Provides distutils command classes for the GRPC Python setup process.""" + +import os +import shutil + +import setuptools + +ROOT_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) +LICENSE = os.path.join(ROOT_DIR, '../../../LICENSE') + + +class Preprocess(setuptools.Command): + """Command to copy LICENSE from root directory.""" + + description = '' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + if os.path.isfile(LICENSE): + shutil.copyfile(LICENSE, os.path.join(ROOT_DIR, 'LICENSE')) diff --git a/tools/run_tests/artifacts/build_artifact_python.sh b/tools/run_tests/artifacts/build_artifact_python.sh index 18eb70c8574..e451ced338f 100755 --- a/tools/run_tests/artifacts/build_artifact_python.sh +++ b/tools/run_tests/artifacts/build_artifact_python.sh @@ -126,7 +126,7 @@ then # Build grpcio_status source distribution ${SETARCH_CMD} "${PYTHON}" src/python/grpcio_status/setup.py \ - preprocess build_package_protos sdist + preprocess sdist cp -r src/python/grpcio_status/dist/* "$ARTIFACT_DIR" fi From 40b8ca97a1d89153711e0d965cd25de1099862f1 Mon Sep 17 00:00:00 2001 From: Lidi Zheng Date: Fri, 14 Dec 2018 10:10:57 -0800 Subject: [PATCH 4/5] Update docstring to make it more clear * Mark API as experimental * Rephrase the raise condition * Add more detail to the returned object --- src/python/grpcio_status/grpc_status/rpc_status.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/python/grpcio_status/grpc_status/rpc_status.py b/src/python/grpcio_status/grpc_status/rpc_status.py index 36c8eba37d5..e23a20968ec 100644 --- a/src/python/grpcio_status/grpc_status/rpc_status.py +++ b/src/python/grpcio_status/grpc_status/rpc_status.py @@ -45,6 +45,8 @@ def _code_to_grpc_status_code(code): def from_call(call): """Returns a google.rpc.status.Status message corresponding to a given grpc.Call. + This is an EXPERIMENTAL API. + Args: call: A grpc.Call instance. @@ -52,8 +54,8 @@ def from_call(call): A google.rpc.status.Status message representing the status of the RPC. Raises: - ValueError: If the status code, status message is inconsistent with the rich status - inside of the google.rpc.status.Status. + ValueError: If the gRPC call's code or details are inconsistent with the + status code and message inside of the google.rpc.status.Status. """ for key, value in call.trailing_metadata(): if key == _GRPC_DETAILS_METADATA_KEY: @@ -74,12 +76,14 @@ def from_call(call): def to_status(status): """Convert a google.rpc.status.Status message to grpc.Status. + This is an EXPERIMENTAL API. + Args: status: a google.rpc.status.Status message representing the non-OK status to terminate the RPC with and communicate it to the client. Returns: - A grpc.Status instance. + A grpc.Status instance representing the input google.rpc.status.Status message. """ return _Status( code=_code_to_grpc_status_code(status.code), From 8621bd47adb7dec1f16bbd616890fd3b0df3336e Mon Sep 17 00:00:00 2001 From: Lidi Zheng Date: Fri, 14 Dec 2018 11:23:18 -0800 Subject: [PATCH 5/5] Assign noop to build_package_protos for backward compatibility --- src/python/grpcio_status/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/python/grpcio_status/setup.py b/src/python/grpcio_status/setup.py index a59cdd0f0f5..983d3ea430b 100644 --- a/src/python/grpcio_status/setup.py +++ b/src/python/grpcio_status/setup.py @@ -69,11 +69,13 @@ try: COMMAND_CLASS = { # Run preprocess from the repository *before* doing any packaging! 'preprocess': _status_commands.Preprocess, + 'build_package_protos': _NoOpCommand, } except ImportError: COMMAND_CLASS = { # wire up commands to no-op not to break the external dependencies 'preprocess': _NoOpCommand, + 'build_package_protos': _NoOpCommand, } setuptools.setup(