Skip to content

Commit d36e896

Browse files
authored
feat: provide and use Python version support check (#832)
1 parent 6c16e96 commit d36e896

File tree

8 files changed

+915
-2
lines changed

8 files changed

+915
-2
lines changed

google/api_core/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@
1717
This package contains common code and utilities used by Google client libraries.
1818
"""
1919

20+
from google.api_core import _python_package_support
21+
from google.api_core import _python_version_support
2022
from google.api_core import version as api_core_version
2123

2224
__version__ = api_core_version.__version__
25+
26+
# NOTE: Until dependent artifacts require this version of
27+
# google.api_core, the functionality below must be made available
28+
# manually in those artifacts.
29+
30+
# expose dependency checks for external callers
31+
check_python_version = _python_version_support.check_python_version
32+
check_dependency_versions = _python_package_support.check_dependency_versions
33+
warn_deprecation_for_versions_less_than = (
34+
_python_package_support.warn_deprecation_for_versions_less_than
35+
)
36+
DependencyConstraint = _python_package_support.DependencyConstraint
37+
38+
# perform version checks against api_core, and emit warnings if needed
39+
check_python_version(package="google.api_core")
40+
check_dependency_versions("google.api_core")
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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+
17+
import warnings
18+
import sys
19+
from typing import Optional
20+
21+
from collections import namedtuple
22+
23+
from ._python_version_support import (
24+
_flatten_message,
25+
_get_distribution_and_import_packages,
26+
)
27+
28+
from 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.
32+
DependencyConstraint = 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+
45+
DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"])
46+
# Version string we provide in a DependencyVersion when we can't determine the version of a
47+
# package.
48+
UNKNOWN_VERSION_STRING = "--"
49+
50+
51+
def 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+
90+
def 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+
184+
def 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+
)

0 commit comments

Comments
 (0)