Skip to content

Commit

Permalink
[Python Stub] Add version check to stubs generated by grpcio_tools (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
XuanWang-Amos authored and Copybara-Service committed Mar 14, 2024
1 parent 2c49416 commit c910004
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 12 deletions.
2 changes: 1 addition & 1 deletion examples/python/helloworld/helloworld_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions examples/python/helloworld/helloworld_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
62 changes: 61 additions & 1 deletion src/compiler/python_generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/python_generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
31 changes: 31 additions & 0 deletions src/python/grpcio/grpc/_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions src/python/grpcio_tests/tests/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/python/grpcio_tests/tests/unit/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
38 changes: 38 additions & 0 deletions src/python/grpcio_tests/tests/unit/_utilities_test.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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()}'
1 change: 1 addition & 0 deletions tools/distrib/python/grpcio_tools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
1 change: 1 addition & 0 deletions tools/distrib/python/grpcio_tools/MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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":
Expand All @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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

17 changes: 17 additions & 0 deletions tools/distrib/python/grpcio_tools/grpc_tools/grpc_version.py
Original file line number Diff line number Diff line change
@@ -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'
15 changes: 11 additions & 4 deletions tools/distrib/python/grpcio_tools/grpc_tools/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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-");

Expand All @@ -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.");
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions tools/distrib/python/grpcio_tools/grpc_tools/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

0 comments on commit c910004

Please sign in to comment.