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 ["package_name", "minimum_fully_supported_version", "recommended_version"],
34)
35_PACKAGE_DEPENDENCY_WARNINGS = [
36 DependencyConstraint(
37 "google.protobuf",
38 minimum_fully_supported_version="4.25.8",
39 recommended_version="6.x",
40 )
41]
42
43
44DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"])
45# Version string we provide in a DependencyVersion when we can't determine the version of a
46# package.
47UNKNOWN_VERSION_STRING = "--"
48
49
50def parse_version_to_tuple(version_string: str) -> ParsedVersion:
51 """Safely converts a semantic version string to a comparable tuple of integers.
52
53 Example: "4.25.8" -> (4, 25, 8)
54 Ignores non-numeric parts and handles common version formats.
55
56 Args:
57 version_string: Version string in the format "x.y.z" or "x.y.z<suffix>"
58
59 Returns:
60 Tuple of integers for the parsed version string.
61 """
62 parts = []
63 for part in version_string.split("."):
64 try:
65 parts.append(int(part))
66 except ValueError:
67 # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
68 # This is a simplification compared to 'packaging.parse_version', but sufficient
69 # for comparing strictly numeric semantic versions.
70 break
71 return tuple(parts)
72
73
74def get_dependency_version(
75 dependency_name: str,
76) -> DependencyVersion:
77 """Get the parsed version of an installed package dependency.
78
79 This function checks for an installed package and returns its version
80 as a comparable tuple of integers object for safe comparison.
81
82 Args:
83 dependency_name: The distribution name of the package (e.g., 'requests').
84
85 Returns:
86 A DependencyVersion namedtuple with `version` (a tuple of integers) and
87 `version_string` attributes, or `DependencyVersion(None,
88 UNKNOWN_VERSION_STRING)` if the package is not found or
89 another error occurs during version discovery.
90
91 """
92 try:
93 version_string: str = metadata.version(dependency_name)
94 parsed_version = parse_version_to_tuple(version_string)
95 return DependencyVersion(parsed_version, version_string)
96 except Exception:
97 # Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
98 # or errors during parse_version_to_tuple
99 return DependencyVersion(None, UNKNOWN_VERSION_STRING)
100
101
102def warn_deprecation_for_versions_less_than(
103 consumer_import_package: str,
104 dependency_import_package: str,
105 minimum_fully_supported_version: str,
106 recommended_version: Optional[str] = None,
107 message_template: Optional[str] = None,
108):
109 """Issue any needed deprecation warnings for `dependency_import_package`.
110
111 If `dependency_import_package` is installed at a version less than
112 `minimum_fully_supported_version`, this issues a warning using either a
113 default `message_template` or one provided by the user. The
114 default `message_template` informs the user that they will not receive
115 future updates for `consumer_import_package` if
116 `dependency_import_package` is somehow pinned to a version lower
117 than `minimum_fully_supported_version`.
118
119 Args:
120 consumer_import_package: The import name of the package that
121 needs `dependency_import_package`.
122 dependency_import_package: The import name of the dependency to check.
123 minimum_fully_supported_version: The dependency_import_package version number
124 below which a deprecation warning will be logged.
125 recommended_version: If provided, the recommended next version, which
126 could be higher than `minimum_fully_supported_version`.
127 message_template: A custom default message template to replace
128 the default. This `message_template` is treated as an
129 f-string, where the following variables are defined:
130 `dependency_import_package`, `consumer_import_package` and
131 `dependency_distribution_package` and
132 `consumer_distribution_package` and `dependency_package`,
133 `consumer_package` , which contain the import packages, the
134 distribution packages, and pretty string with both the
135 distribution and import packages for the dependency and the
136 consumer, respectively; and `minimum_fully_supported_version`,
137 `version_used`, and `version_used_string`, which refer to supported
138 and currently-used versions of the dependency.
139
140 """
141 if (
142 not consumer_import_package
143 or not dependency_import_package
144 or not minimum_fully_supported_version
145 ): # pragma: NO COVER
146 return
147
148 dependency_version = get_dependency_version(dependency_import_package)
149 if not dependency_version.version:
150 return
151
152 if dependency_version.version < parse_version_to_tuple(
153 minimum_fully_supported_version
154 ):
155 (
156 dependency_package,
157 dependency_distribution_package,
158 ) = _get_distribution_and_import_packages(dependency_import_package)
159 (
160 consumer_package,
161 consumer_distribution_package,
162 ) = _get_distribution_and_import_packages(consumer_import_package)
163
164 recommendation = (
165 " (we recommend {recommended_version})" if recommended_version else ""
166 )
167 message_template = message_template or _flatten_message(
168 """
169 DEPRECATION: Package {consumer_package} depends on
170 {dependency_package}, currently installed at version
171 {version_used_string}. Future updates to
172 {consumer_package} will require {dependency_package} at
173 version {minimum_fully_supported_version} or
174 higher{recommendation}. Please ensure that either (a) your
175 Python environment doesn't pin the version of
176 {dependency_package}, so that updates to
177 {consumer_package} can require the higher version, or (b)
178 you manually update your Python environment to use at
179 least version {minimum_fully_supported_version} of
180 {dependency_package}.
181 """
182 )
183 warnings.warn(
184 message_template.format(
185 consumer_import_package=consumer_import_package,
186 dependency_import_package=dependency_import_package,
187 consumer_distribution_package=consumer_distribution_package,
188 dependency_distribution_package=dependency_distribution_package,
189 dependency_package=dependency_package,
190 consumer_package=consumer_package,
191 minimum_fully_supported_version=minimum_fully_supported_version,
192 recommendation=recommendation,
193 version_used=dependency_version.version,
194 version_used_string=dependency_version.version_string,
195 ),
196 FutureWarning,
197 )
198
199
200def check_dependency_versions(
201 consumer_import_package: str, *package_dependency_warnings: DependencyConstraint
202):
203 """Bundle checks for all package dependencies.
204
205 This function can be called by all consumers of google.api_core,
206 to emit needed deprecation warnings for any of their
207 dependencies. The dependencies to check can be passed as arguments, or if
208 none are provided, it will default to the list in
209 `_PACKAGE_DEPENDENCY_WARNINGS`.
210
211 Args:
212 consumer_import_package: The distribution name of the calling package, whose
213 dependencies we're checking.
214 *package_dependency_warnings: A variable number of DependencyConstraint
215 objects, each specifying a dependency to check.
216 """
217 if not package_dependency_warnings:
218 package_dependency_warnings = tuple(_PACKAGE_DEPENDENCY_WARNINGS)
219 for package_info in package_dependency_warnings:
220 warn_deprecation_for_versions_less_than(
221 consumer_import_package,
222 package_info.package_name,
223 package_info.minimum_fully_supported_version,
224 recommended_version=package_info.recommended_version,
225 )