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