1# Copyright 2025 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Code to check versions of dependencies used by Google Cloud Client Libraries."""
16
17import warnings
18from collections import namedtuple
19from importlib import metadata
20from typing import Optional, Tuple
21
22from ._python_version_support import (
23 _flatten_message,
24 _get_distribution_and_import_packages,
25)
26
27ParsedVersion = Tuple[int, ...]
28
29# Here we list all the packages for which we want to issue warnings
30# about deprecated and unsupported versions.
31DependencyConstraint = namedtuple(
32 "DependencyConstraint",
33 [
34 "package_name",
35 "minimum_fully_supported_version",
36 "recommended_version",
37 "message_template",
38 ],
39 defaults=(None, None),
40)
41
42PQC_GRPC_WARNING_TEMPLATE = (
43 "Package {consumer_package} depends on {dependency_package}, currently installed at version {version_used_string}. "
44 "grpcio < 1.83.0 does not support Post-Quantum Cryptography (PQC). "
45 "Support for non-PQC environments is deprecated. In October 2026, "
46 "Google Cloud Python packages will raise their minimum requirements "
47 "(including google-api-core, grpcio, and grpcio-status) to enforce grpcio >= 1.83.0. "
48 "For more details on Google Cloud's post-quantum security migration, visit: "
49 "https://cloud.google.com/security/resources/post-quantum-cryptography"
50)
51
52_PACKAGE_DEPENDENCY_WARNINGS = [
53 DependencyConstraint(
54 "google.protobuf",
55 minimum_fully_supported_version="6.33.5",
56 recommended_version="6.x",
57 ),
58 DependencyConstraint(
59 "grpcio",
60 minimum_fully_supported_version="1.83.0",
61 recommended_version="1.83.x",
62 message_template=PQC_GRPC_WARNING_TEMPLATE,
63 ),
64]
65
66
67DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"])
68# Version string we provide in a DependencyVersion when we can't determine the version of a
69# package.
70UNKNOWN_VERSION_STRING = "--"
71
72
73def parse_version_to_tuple(version_string: str) -> ParsedVersion:
74 """Safely converts a semantic version string to a comparable tuple of integers.
75
76 Example: "6.33.5" -> (6, 33, 5)
77 Ignores non-numeric parts and handles common version formats.
78
79 Args:
80 version_string: Version string in the format "x.y.z" or "x.y.z<suffix>"
81
82 Returns:
83 Tuple of integers for the parsed version string.
84 """
85 parts = []
86 for part in version_string.split("."):
87 try:
88 parts.append(int(part))
89 except ValueError:
90 # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
91 # This is a simplification compared to 'packaging.parse_version', but sufficient
92 # for comparing strictly numeric semantic versions.
93 break
94 return tuple(parts)
95
96
97def get_dependency_version(
98 dependency_name: str,
99) -> DependencyVersion:
100 """Get the parsed version of an installed package dependency.
101
102 This function checks for an installed package and returns its version
103 as a comparable tuple of integers object for safe comparison.
104
105 Args:
106 dependency_name: The distribution name of the package (e.g., 'requests').
107
108 Returns:
109 A DependencyVersion namedtuple with `version` (a tuple of integers) and
110 `version_string` attributes, or `DependencyVersion(None,
111 UNKNOWN_VERSION_STRING)` if the package is not found or
112 another error occurs during version discovery.
113
114 """
115 try:
116 version_string: str = metadata.version(dependency_name)
117 parsed_version = parse_version_to_tuple(version_string)
118 return DependencyVersion(parsed_version, version_string)
119 except Exception:
120 # Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
121 # or errors during parse_version_to_tuple
122 return DependencyVersion(None, UNKNOWN_VERSION_STRING)
123
124
125def warn_deprecation_for_versions_less_than(
126 consumer_import_package: str,
127 dependency_import_package: str,
128 minimum_fully_supported_version: str,
129 recommended_version: Optional[str] = None,
130 message_template: Optional[str] = None,
131):
132 """Issue any needed deprecation warnings for `dependency_import_package`.
133
134 If `dependency_import_package` is installed at a version less than
135 `minimum_fully_supported_version`, this issues a warning using either a
136 default `message_template` or one provided by the user. The
137 default `message_template` informs the user that they will not receive
138 future updates for `consumer_import_package` if
139 `dependency_import_package` is somehow pinned to a version lower
140 than `minimum_fully_supported_version`.
141
142 Args:
143 consumer_import_package: The import name of the package that
144 needs `dependency_import_package`.
145 dependency_import_package: The import name of the dependency to check.
146 minimum_fully_supported_version: The dependency_import_package version number
147 below which a deprecation warning will be logged.
148 recommended_version: If provided, the recommended next version, which
149 could be higher than `minimum_fully_supported_version`.
150 message_template: A custom default message template to replace
151 the default. This `message_template` is treated as an
152 f-string, where the following variables are defined:
153 `dependency_import_package`, `consumer_import_package` and
154 `dependency_distribution_package` and
155 `consumer_distribution_package` and `dependency_package`,
156 `consumer_package` , which contain the import packages, the
157 distribution packages, and pretty string with both the
158 distribution and import packages for the dependency and the
159 consumer, respectively; and `minimum_fully_supported_version`,
160 `version_used`, and `version_used_string`, which refer to supported
161 and currently-used versions of the dependency.
162
163 """
164 if (
165 not consumer_import_package
166 or not dependency_import_package
167 or not minimum_fully_supported_version
168 ): # pragma: NO COVER
169 return
170
171 dependency_version = get_dependency_version(dependency_import_package)
172 if not dependency_version.version:
173 return
174
175 if dependency_version.version < parse_version_to_tuple(
176 minimum_fully_supported_version
177 ):
178 (
179 dependency_package,
180 dependency_distribution_package,
181 ) = _get_distribution_and_import_packages(dependency_import_package)
182 (
183 consumer_package,
184 consumer_distribution_package,
185 ) = _get_distribution_and_import_packages(consumer_import_package)
186
187 recommendation = (
188 f" (we recommend {recommended_version})" if recommended_version else ""
189 )
190 message_template = message_template or _flatten_message(
191 """
192 DEPRECATION: Package {consumer_package} depends on
193 {dependency_package}, currently installed at version
194 {version_used_string}. Future updates to
195 {consumer_package} will require {dependency_package} at
196 version {minimum_fully_supported_version} or
197 higher{recommendation}. Please ensure that either (a) your
198 Python environment doesn't pin the version of
199 {dependency_package}, so that updates to
200 {consumer_package} can require the higher version, or (b)
201 you manually update your Python environment to use at
202 least version {minimum_fully_supported_version} of
203 {dependency_package}.
204 """
205 )
206 warnings.warn(
207 message_template.format(
208 consumer_import_package=consumer_import_package,
209 dependency_import_package=dependency_import_package,
210 consumer_distribution_package=consumer_distribution_package,
211 dependency_distribution_package=dependency_distribution_package,
212 dependency_package=dependency_package,
213 consumer_package=consumer_package,
214 minimum_fully_supported_version=minimum_fully_supported_version,
215 recommendation=recommendation,
216 version_used=dependency_version.version,
217 version_used_string=dependency_version.version_string,
218 ),
219 FutureWarning,
220 )
221
222
223def check_dependency_versions(
224 consumer_import_package: str, *package_dependency_warnings: DependencyConstraint
225):
226 """Bundle checks for all package dependencies.
227
228 This function can be called by all consumers of google.api_core,
229 to emit needed deprecation warnings for any of their
230 dependencies. The dependencies to check can be passed as arguments, or if
231 none are provided, it will default to the list in
232 `_PACKAGE_DEPENDENCY_WARNINGS`.
233
234 Args:
235 consumer_import_package: The distribution name of the calling package, whose
236 dependencies we're checking.
237 *package_dependency_warnings: A variable number of DependencyConstraint
238 objects, each specifying a dependency to check.
239 """
240 if not package_dependency_warnings:
241 package_dependency_warnings = tuple(_PACKAGE_DEPENDENCY_WARNINGS)
242 for package_info in package_dependency_warnings:
243 warn_deprecation_for_versions_less_than(
244 consumer_import_package,
245 package_info.package_name,
246 package_info.minimum_fully_supported_version,
247 recommended_version=package_info.recommended_version,
248 message_template=package_info.message_template,
249 )