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