[Python Stub] Add version check to stubs generated by grpcio_tools (#35906)

The stubs generated by grpcio_tools should always be used with [the same or higher version of grpcio](https://github.com/grpc/grpc/blob/master/tools/distrib/python/grpcio_tools/setup.py#L313), this change will add a run time check for this requirement inside the generated stubs and therefor enforce this requirement.

Please note for now we're just printing a warning for incorrect usage, we'll **change it to an error** soon.

Example warning message:
```
/usr/local/google/home/xuanwn/workspace/misc/grpc/examples/python/helloworld/helloworld_pb2_grpc.py:21: RuntimeWarning: The grpc package installed is at version 1.60.1, but the generated code in helloworld_pb2_grpc.py depends on grpcio>=1.63.0.dev0. Please upgrade your grpc module to grpcio>=1.63.0.dev0 or downgrade your generated code using grpcio-tools<=1.60.1. This warning will become an error in 1.64.0, scheduled for release on May 14,2024.
```
<!--

If you know who should review your pull request, please assign it to that
person, otherwise the pull request would get assigned randomly.

If your pull request is for a specific language, please add the appropriate
lang label.

-->

Closes #35906

PiperOrigin-RevId: 615659471
pull/36077/head
Xuan Wang 11 months ago committed by Copybara-Service
parent 2c49416713
commit c910004328
  1. 2
      examples/python/helloworld/helloworld_pb2.py
  2. 25
      examples/python/helloworld/helloworld_pb2_grpc.py
  3. 62
      src/compiler/python_generator.cc
  4. 2
      src/compiler/python_generator.h
  5. 31
      src/python/grpcio/grpc/_utilities.py
  6. 1
      src/python/grpcio_tests/tests/tests.json
  7. 1
      src/python/grpcio_tests/tests/unit/BUILD.bazel
  8. 38
      src/python/grpcio_tests/tests/unit/_utilities_test.py
  9. 19
      templates/tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py.template
  10. 1
      tools/distrib/python/grpcio_tools/BUILD.bazel
  11. 1
      tools/distrib/python/grpcio_tools/MANIFEST.in
  12. 12
      tools/distrib/python/grpcio_tools/grpc_tools/_protoc_compiler.pyx
  13. 17
      tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py
  14. 15
      tools/distrib/python/grpcio_tools/grpc_tools/main.cc
  15. 5
      tools/distrib/python/grpcio_tools/grpc_tools/main.h

@ -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

@ -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.

@ -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<bool, std::string> 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) {}

@ -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<std::string> prefixes_to_filter;
};

@ -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)

@ -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",

@ -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",
]

@ -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)

@ -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()}'

@ -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"],

@ -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

@ -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 = <char **>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

@ -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'

@ -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<std::string>* include_paths,
char* protobuf_path, char* version,
const std::vector<std::string>* include_paths,
std::vector<std::pair<std::string, std::string>>* 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);

@ -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<ProtocError>* errors, std::vector<ProtocWarning>* warnings);
int protoc_get_services(
char* protobuf_path, const std::vector<std::string>* include_paths,
char* protobuf_path, char* version,
const std::vector<std::string>* include_paths,
std::vector<std::pair<std::string, std::string>>* files_out,
std::vector<ProtocError>* errors, std::vector<ProtocWarning>* warnings);
} // end namespace grpc_tools

Loading…
Cancel
Save