diff --git a/examples/python/helloworld/helloworld_pb2.py b/examples/python/helloworld/helloworld_pb2.py index 6ee31ad94a229..0e202a0911695 100644 --- a/examples/python/helloworld/helloworld_pb2.py +++ b/examples/python/helloworld/helloworld_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: helloworld.proto -# Protobuf Python Version: 4.25.0 +# Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/examples/python/helloworld/helloworld_pb2_grpc.py b/examples/python/helloworld/helloworld_pb2_grpc.py index 68bcfef17566a..48ad1004f9b0f 100644 --- a/examples/python/helloworld/helloworld_pb2_grpc.py +++ b/examples/python/helloworld/helloworld_pb2_grpc.py @@ -1,9 +1,34 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings import helloworld_pb2 as helloworld__pb2 +GRPC_GENERATED_VERSION = '1.63.0.dev0' +GRPC_VERSION = grpc.__version__ +EXPECTED_ERROR_RELEASE = '1.65.0' +SCHEDULED_RELEASE_DATE = 'June 25, 2024' +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + warnings.warn( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in helloworld_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', + RuntimeWarning + ) + class GreeterStub(object): """The greeting service definition. diff --git a/src/compiler/python_generator.cc b/src/compiler/python_generator.cc index b7a3115bce09b..53b6083c6700f 100644 --- a/src/compiler/python_generator.cc +++ b/src/compiler/python_generator.cc @@ -688,6 +688,9 @@ bool PrivateGenerator::PrintPreamble(grpc_generator::Printer* out) { StringMap var; var["Package"] = config.grpc_package_root; out->Print(var, "import $Package$\n"); + if (config.grpc_tools_version.size() > 0) { + out->Print(var, "import warnings\n"); + } if (generate_in_pb2_grpc) { out->Print("\n"); StringPairSet imports_set; @@ -732,6 +735,56 @@ bool PrivateGenerator::PrintPreamble(grpc_generator::Printer* out) { } out->Print(var, "$ImportStatement$ as $ModuleAlias$\n"); } + + // Checks if generate code is used with a supported grpcio version. + if (config.grpc_tools_version.size() > 0) { + var["ToolsVersion"] = config.grpc_tools_version; + out->Print(var, "\nGRPC_GENERATED_VERSION = '$ToolsVersion$'\n"); + out->Print("GRPC_VERSION = grpc.__version__\n"); + out->Print("EXPECTED_ERROR_RELEASE = '1.65.0'\n"); + out->Print("SCHEDULED_RELEASE_DATE = 'June 25, 2024'\n"); + out->Print("_version_not_supported = False\n\n"); + out->Print("try:\n"); + { + IndentScope raii_import_indent(out); + out->Print( + "from grpc._utilities import first_version_is_lower\n" + "_version_not_supported = first_version_is_lower(GRPC_VERSION, " + "GRPC_GENERATED_VERSION)\n"); + } + out->Print("except ImportError:\n"); + { + IndentScope raii_import_error_indent(out); + out->Print("_version_not_supported = True\n"); + } + out->Print("\nif _version_not_supported:\n"); + { + IndentScope raii_warning_indent(out); + out->Print("warnings.warn(\n"); + { + IndentScope raii_warning_string_indent(out); + std::string filename_without_ext = file->filename_without_ext(); + std::replace(filename_without_ext.begin(), filename_without_ext.end(), + '-', '_'); + var["Pb2GrpcFileName"] = filename_without_ext; + out->Print( + var, + "f'The grpc package installed is at version {GRPC_VERSION},'\n" + "+ f' but the generated code in $Pb2GrpcFileName$_pb2_grpc.py " + "depends on'\n" + "+ f' grpcio>={GRPC_GENERATED_VERSION}.'\n" + "+ f' Please upgrade your grpc module to " + "grpcio>={GRPC_GENERATED_VERSION}'\n" + "+ f' or downgrade your generated code using " + "grpcio-tools<={GRPC_VERSION}.'\n" + "+ f' This warning will become an error in " + "{EXPECTED_ERROR_RELEASE},'\n" + "+ f' scheduled for release on {SCHEDULED_RELEASE_DATE}.',\n" + "RuntimeWarning\n"); + } + out->Print(")\n"); + } + } } return true; } @@ -828,7 +881,14 @@ pair PrivateGenerator::GetGrpcServices() { GeneratorConfiguration::GeneratorConfiguration() : grpc_package_root("grpc"), beta_package_root("grpc.beta"), - import_prefix("") {} + import_prefix(""), + grpc_tools_version("") {} + +GeneratorConfiguration::GeneratorConfiguration(std::string version) + : grpc_package_root("grpc"), + beta_package_root("grpc.beta"), + import_prefix(""), + grpc_tools_version(version) {} PythonGrpcGenerator::PythonGrpcGenerator(const GeneratorConfiguration& config) : config_(config) {} diff --git a/src/compiler/python_generator.h b/src/compiler/python_generator.h index 12343ccde176d..d65f310f64d79 100644 --- a/src/compiler/python_generator.h +++ b/src/compiler/python_generator.h @@ -31,11 +31,13 @@ namespace grpc_python_generator { // that may be used internally at Google. struct GeneratorConfiguration { GeneratorConfiguration(); + GeneratorConfiguration(std::string version); std::string grpc_package_root; // TODO(https://github.com/grpc/grpc/issues/8622): Drop this. std::string beta_package_root; // TODO(https://github.com/protocolbuffers/protobuf/issues/888): Drop this. std::string import_prefix; + std::string grpc_tools_version; std::vector prefixes_to_filter; }; diff --git a/src/python/grpcio/grpc/_utilities.py b/src/python/grpcio/grpc/_utilities.py index 1ab8c4fa8f133..620cab3838cf0 100644 --- a/src/python/grpcio/grpc/_utilities.py +++ b/src/python/grpcio/grpc/_utilities.py @@ -189,3 +189,34 @@ def channel_ready_future(channel: grpc.Channel) -> _ChannelReadyFuture: ready_future = _ChannelReadyFuture(channel) ready_future.start() return ready_future + + +def first_version_is_lower(version1: str, version2: str) -> bool: + """ + Compares two versions in the format '1.60.1' or '1.60.1.dev0'. + + This method will be used in all stubs generated by grpcio-tools to check whether + the stub version is compatible with the runtime grpcio. + + Args: + version1: The first version string. + version2: The second version string. + + Returns: + True if version1 is lower, False otherwise. + """ + version1_list = version1.split(".") + version2_list = version2.split(".") + + try: + for i in range(3): + if int(version1_list[i]) < int(version2_list[i]): + return True + elif int(version1_list[i]) > int(version2_list[i]): + return False + except ValueError: + # Return false in case we can't convert version to int. + return False + + # The version without dev0 will be considered lower. + return len(version1_list) < len(version2_list) diff --git a/src/python/grpcio_tests/tests/tests.json b/src/python/grpcio_tests/tests/tests.json index 9c42ec7baa069..32406fd883361 100644 --- a/src/python/grpcio_tests/tests/tests.json +++ b/src/python/grpcio_tests/tests/tests.json @@ -84,6 +84,7 @@ "tests.unit._server_wait_for_termination_test.ServerWaitForTerminationTest", "tests.unit._session_cache_test.SSLSessionCacheTest", "tests.unit._signal_handling_test.SignalHandlingTest", + "tests.unit._utilities_test.UtilityTest", "tests.unit._version_test.VersionTest", "tests.unit._xds_credentials_test.XdsCredentialsTest", "tests.unit.beta._beta_features_test.BetaFeaturesTest", diff --git a/src/python/grpcio_tests/tests/unit/BUILD.bazel b/src/python/grpcio_tests/tests/unit/BUILD.bazel index 400db0fb55bb0..de04e87b41af7 100644 --- a/src/python/grpcio_tests/tests/unit/BUILD.bazel +++ b/src/python/grpcio_tests/tests/unit/BUILD.bazel @@ -54,6 +54,7 @@ GRPCIO_TESTS_UNIT = [ "_server_shutdown_test.py", "_server_wait_for_termination_test.py", "_session_cache_test.py", + "_utilities_test.py", "_xds_credentials_test.py", ] diff --git a/src/python/grpcio_tests/tests/unit/_utilities_test.py b/src/python/grpcio_tests/tests/unit/_utilities_test.py new file mode 100644 index 0000000000000..7cef5f8781355 --- /dev/null +++ b/src/python/grpcio_tests/tests/unit/_utilities_test.py @@ -0,0 +1,38 @@ +# Copyright 2024 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 gRPC Python's utilities.""" + +import logging +import unittest + +from grpc._utilities import first_version_is_lower + + +class UtilityTest(unittest.TestCase): + def testVersionCheck(self): + self.assertTrue(first_version_is_lower("1.2.3", "1.2.4")) + self.assertTrue(first_version_is_lower("1.2.4", "10.2.3")) + self.assertTrue(first_version_is_lower("1.2.3", "1.2.3.dev0")) + self.assertFalse(first_version_is_lower("NOT_A_VERSION", "1.2.4")) + self.assertFalse(first_version_is_lower("1.2.3", "NOT_A_VERSION")) + self.assertFalse(first_version_is_lower("1.2.4", "1.2.3")) + self.assertFalse(first_version_is_lower("10.2.3", "1.2.4")) + self.assertFalse(first_version_is_lower("1.2.3dev0", "1.2.3")) + self.assertFalse(first_version_is_lower("1.2.3", "1.2.3dev0")) + self.assertFalse(first_version_is_lower("1.2.3.dev0", "1.2.3")) + + +if __name__ == "__main__": + logging.basicConfig() + unittest.main(verbosity=2) diff --git a/templates/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py.template b/templates/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py.template new file mode 100644 index 0000000000000..de03b9bfbcc64 --- /dev/null +++ b/templates/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py.template @@ -0,0 +1,19 @@ +%YAML 1.2 +--- | + # Copyright 2024 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/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py.template`!!! + + VERSION = '${settings.python_version.pep440()}' diff --git a/tools/distrib/python/grpcio_tools/BUILD.bazel b/tools/distrib/python/grpcio_tools/BUILD.bazel index fac6bb3babd23..ba24e779e7a78 100644 --- a/tools/distrib/python/grpcio_tools/BUILD.bazel +++ b/tools/distrib/python/grpcio_tools/BUILD.bazel @@ -50,6 +50,7 @@ py_library( name = "grpc_tools", srcs = [ "grpc_tools/__init__.py", + "grpc_tools/grpc_version.py", "grpc_tools/protoc.py", ], data = [":well_known_protos"], diff --git a/tools/distrib/python/grpcio_tools/MANIFEST.in b/tools/distrib/python/grpcio_tools/MANIFEST.in index 4943751879ec2..acd97f57ef31a 100644 --- a/tools/distrib/python/grpcio_tools/MANIFEST.in +++ b/tools/distrib/python/grpcio_tools/MANIFEST.in @@ -3,6 +3,7 @@ include grpc_version.py include protoc_deps.py include protoc_lib_deps.py include README.rst +include grpc_tools/grpc_version.py graft grpc_tools graft grpc_root graft third_party diff --git a/tools/distrib/python/grpcio_tools/grpc_tools/_protoc_compiler.pyx b/tools/distrib/python/grpcio_tools/grpc_tools/_protoc_compiler.pyx index 7b307e0475cac..a69e6dc74e516 100644 --- a/tools/distrib/python/grpcio_tools/grpc_tools/_protoc_compiler.pyx +++ b/tools/distrib/python/grpcio_tools/grpc_tools/_protoc_compiler.pyx @@ -13,6 +13,7 @@ # limitations under the License. # distutils: language=c++ +cimport cpython from cython.operator cimport dereference from libc cimport stdlib from libcpp.string cimport string @@ -21,6 +22,8 @@ from libcpp.vector cimport vector import warnings +from grpc_tools import grpc_version + cdef extern from "grpc_tools/main.h" namespace "grpc_tools": cppclass cProtocError "::grpc_tools::ProtocError": @@ -35,13 +38,13 @@ cdef extern from "grpc_tools/main.h" namespace "grpc_tools": int column string message - int protoc_main(int argc, char *argv[]) + int protoc_main(int argc, char *argv[], char* version) int protoc_get_protos(char* protobuf_path, vector[string]* include_path, vector[pair[string, string]]* files_out, vector[cProtocError]* errors, vector[cProtocWarning]* wrnings) nogil except + - int protoc_get_services(char* protobuf_path, + int protoc_get_services(char* protobuf_path, char* version, vector[string]* include_path, vector[pair[string, string]]* files_out, vector[cProtocError]* errors, @@ -51,7 +54,7 @@ def run_main(list args not None): cdef char **argv = stdlib.malloc(len(args)*sizeof(char *)) for i in range(len(args)): argv[i] = args[i] - return protoc_main(len(args), argv) + return protoc_main(len(args), argv, grpc_version.VERSION.encode()) class ProtocError(Exception): def __init__(self, filename, line, column, message): @@ -129,7 +132,8 @@ def get_services(bytes protobuf_path, list include_paths): cdef vector[cProtocError] errors # NOTE: Abbreviated name used to avoid shadowing of the module name. cdef vector[cProtocWarning] wrnings - rc = protoc_get_services(protobuf_path, &c_include_paths, &files, &errors, &wrnings) + version = grpc_version.VERSION.encode() + rc = protoc_get_services(protobuf_path, version, &c_include_paths, &files, &errors, &wrnings) _handle_errors(rc, &errors, &wrnings, protobuf_path) return files diff --git a/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py b/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py new file mode 100644 index 0000000000000..1855d69da7361 --- /dev/null +++ b/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py @@ -0,0 +1,17 @@ +# Copyright 2024 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/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py.template`!!! + +VERSION = '1.63.0.dev0' diff --git a/tools/distrib/python/grpcio_tools/grpc_tools/main.cc b/tools/distrib/python/grpcio_tools/grpc_tools/main.cc index 9302cc75529f1..866b1e2866b37 100644 --- a/tools/distrib/python/grpcio_tools/grpc_tools/main.cc +++ b/tools/distrib/python/grpcio_tools/grpc_tools/main.cc @@ -43,7 +43,7 @@ using ::google::protobuf::io::StringOutputStream; using ::google::protobuf::io::ZeroCopyOutputStream; namespace grpc_tools { -int protoc_main(int argc, char* argv[]) { +int protoc_main(int argc, char* argv[], char* version) { google::protobuf::compiler::CommandLineInterface cli; cli.AllowPlugins("protoc-"); @@ -57,8 +57,12 @@ int protoc_main(int argc, char* argv[]) { cli.RegisterGenerator("--pyi_out", &pyi_generator, "Generate Python pyi stub."); + // Get grpc_tools version + std::string grpc_tools_version = version; + // gRPC Python - grpc_python_generator::GeneratorConfiguration grpc_py_config; + grpc_python_generator::GeneratorConfiguration grpc_py_config( + grpc_tools_version); grpc_python_generator::PythonGrpcGenerator grpc_py_generator(grpc_py_config); cli.RegisterGenerator("--grpc_python_out", &grpc_py_generator, "Generate Python source file."); @@ -181,11 +185,14 @@ int protoc_get_protos( } int protoc_get_services( - char* protobuf_path, const std::vector* include_paths, + char* protobuf_path, char* version, + const std::vector* include_paths, std::vector>* files_out, std::vector<::grpc_tools::ProtocError>* errors, std::vector<::grpc_tools::ProtocWarning>* warnings) { - grpc_python_generator::GeneratorConfiguration grpc_py_config; + std::string grpc_tools_version = version; + grpc_python_generator::GeneratorConfiguration grpc_py_config( + grpc_tools_version); grpc_python_generator::PythonGrpcGenerator grpc_py_generator(grpc_py_config); return generate_code(&grpc_py_generator, protobuf_path, include_paths, files_out, errors, warnings); diff --git a/tools/distrib/python/grpcio_tools/grpc_tools/main.h b/tools/distrib/python/grpcio_tools/grpc_tools/main.h index 7fcce3068c622..847f598c49c96 100644 --- a/tools/distrib/python/grpcio_tools/grpc_tools/main.h +++ b/tools/distrib/python/grpcio_tools/grpc_tools/main.h @@ -19,7 +19,7 @@ namespace grpc_tools { // We declare `protoc_main` here since we want access to it from Cython as an // extern but *without* triggering a dllimport declspec when on Windows. -int protoc_main(int argc, char* argv[]); +int protoc_main(int argc, char* argv[], char* version); struct ProtocError { std::string filename; @@ -40,7 +40,8 @@ int protoc_get_protos( std::vector* errors, std::vector* warnings); int protoc_get_services( - char* protobuf_path, const std::vector* include_paths, + char* protobuf_path, char* version, + const std::vector* include_paths, std::vector>* files_out, std::vector* errors, std::vector* warnings); } // end namespace grpc_tools