Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
05ee669
feat: provide Python run-time version support
vchudnov-g Jul 29, 2025
8d1ade4
feat: apply Python version suport warnings to api_core
vchudnov-g Jul 29, 2025
6d1058d
feat: add deprecation check for the protobuf package
vchudnov-g Jul 30, 2025
0f8caa2
format files
vchudnov-g Jul 30, 2025
3ec85a6
fix lint warning
vchudnov-g Jul 30, 2025
240baec
add docstring to `warn_deprecation_for_versions_less_than`
vchudnov-g Jul 30, 2025
ee4d6b6
Add/fix docstrings
vchudnov-g Jul 30, 2025
0726cd2
fix typo
vchudnov-g Jul 31, 2025
8f54a81
add test for _python_package_support.py
vchudnov-g Jul 31, 2025
9f3d7ef
add constants for various buffer periods
vchudnov-g Jul 31, 2025
b03aa68
Update warning code to only require import package names
vchudnov-g Jul 31, 2025
3bae70e
Fix messaegs and test
vchudnov-g Jul 31, 2025
73de862
Add TODO: provide the functionality in previous versions of api_core
vchudnov-g Jul 31, 2025
1ec82cb
Fix mypy failures
vchudnov-g Aug 12, 2025
79090e4
Try to remove a round-off error causing a test mock failure
vchudnov-g Aug 12, 2025
c123f1c
Remove more potential test failures/warnings
vchudnov-g Aug 12, 2025
1c9aae5
Format
vchudnov-g Aug 12, 2025
b41cd7f
Try making the specified_timeout a float
vchudnov-g Aug 12, 2025
6faca07
fix: tweak message parameter names
vchudnov-g Sep 3, 2025
0d03e1f
fix: add PYTHON_VERSION_STATUS_UNSPECIFIED enum value
vchudnov-g Sep 3, 2025
5a2cce0
docs: tweak TODOs
vchudnov-g Sep 3, 2025
cc41275
fix test to match code changes
vchudnov-g Sep 3, 2025
df92992
fix: restore asyncio designator for async tests
vchudnov-g Sep 4, 2025
81eae62
Remove some workarounds trying to fix presubmit errors (since fixed)
vchudnov-g Sep 5, 2025
d558af4
Additional test tweaks to prevent non-significant failures
vchudnov-g Sep 5, 2025
566b933
chore: fix test that was waiting before initiating operation
vchudnov-g Sep 5, 2025
6adc2f3
fix: skip coverage checks for code specific to Pyton 3.7
vchudnov-g Sep 5, 2025
e957094
chore: try to address coverage failures
vchudnov-g Sep 8, 2025
0cf15f2
chore: try to address more coverage failures
vchudnov-g Sep 8, 2025
d60d12d
chore: fix comments and exported function
vchudnov-g Sep 9, 2025
f3edf46
fix lint
vchudnov-g Sep 9, 2025
d22c43c
chore: fix unit test
vchudnov-g Sep 9, 2025
0fdd8bc
chore: lint
vchudnov-g Sep 9, 2025
38ded4e
Use warnings module instead of logging module
vchudnov-g Sep 10, 2025
7dc906d
fix: print the current package versions correctly
vchudnov-g Sep 15, 2025
2de943a
wip: before gemini doc fixes
vchudnov-g Sep 15, 2025
e61455a
fix: return tuple of version tuple and version string, fix tests
vchudnov-g Sep 16, 2025
ce216e4
lint
vchudnov-g Sep 22, 2025
695bfcf
feat: add grace period for 3.9; tweak warning
vchudnov-g Oct 1, 2025
eebbe61
lint
vchudnov-g Oct 1, 2025
670ba62
allow providing a recommended version
vchudnov-g Oct 1, 2025
2dc8f5f
refactor: used namedtuple
vchudnov-g Oct 13, 2025
f23c41b
refactor: s/"--"/UNKNOWN_VERSION_STRING/ in code, not test
vchudnov-g Oct 13, 2025
691981e
refactor s/next_supported_version/minimum_fully_supported_version/
vchudnov-g Oct 13, 2025
d13feb1
refactor: s/dependent_/consumer_/
vchudnov-g Oct 13, 2025
6493e33
refactor: add _PACKAGE_DEPENDENCY_WARNINGS
vchudnov-g Oct 13, 2025
c51d61b
refactor: reorder and comment on _init__ statements for clarity
vchudnov-g Oct 13, 2025
706518c
fix: exapnd VersionInfo with version field; clarify fake dates
vchudnov-g Oct 13, 2025
a254693
doc: document PythonVersionSupportStatus
vchudnov-g Oct 13, 2025
8d19eb2
refactor: make fake past and future versions into constants
vchudnov-g Oct 14, 2025
8ad6f2b
refactor: reword default string for min_python
vchudnov-g Oct 14, 2025
a9c9f15
fix: fix populating PYTHON_VERSION_INFO
vchudnov-g Oct 14, 2025
aa41540
fix: type hints
vchudnov-g Oct 14, 2025
518266e
feat: allow check_dependency_versions() to take any number of Depende…
vchudnov-g Oct 17, 2025
9ad1d42
add unit test to satisfy coverage requirement
vchudnov-g Oct 17, 2025
703a5b7
fix lint
vchudnov-g Oct 17, 2025
66968d2
fix lint
vchudnov-g Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions google/api_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@
This package contains common code and utilities used by Google client libraries.
"""

from google.api_core import _python_package_support
from google.api_core import _python_version_support
from google.api_core import version as api_core_version

__version__ = api_core_version.__version__

# NOTE: Until dependent artifacts require this version of
# google.api_core, the functionality below must be made available
# manually in those artifacts.

# expose dependency checks for external callers
check_python_version = _python_version_support.check_python_version
check_dependency_versions = _python_package_support.check_dependency_versions
warn_deprecation_for_versions_less_than = (
_python_package_support.warn_deprecation_for_versions_less_than
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are both check_dependency_versions and warn_deprecation_for_versions_less_than meant to be used externally? I'm just seeing check_dependency_versions in the generator PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. check_dependency_versions calls warn_deprecation_for_versions_less_than, so for package deprecation warnings that apply to anything used by api_core as well as its dependents, such as the GAPICs, calling the former is sufficient. At the moment, the only such package is protobuf.
  2. Modules can import and use warn_deprecation_for_versions_less_than directly for packages that they themselves want to issue deprecation warnings for, but which are not used by api_core itself. We are not configuring any right now.

For those two reasons,

  • the generator PR is not using warn_deprecation_for_versions_less_than at the moment, and
  • warn_deprecation_for_versions_less_than does not have an underscore prefix.

I think this design makes sense, but please do check my thinking (and my explanation).

DependencyConstraint = _python_package_support.DependencyConstraint

# perform version checks against api_core, and emit warnings if needed
check_python_version(package="google.api_core")
check_dependency_versions("google.api_core")
209 changes: 209 additions & 0 deletions google/api_core/_python_package_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Code to check versions of dependencies used by Google Cloud Client Libraries."""

import warnings
import sys
from typing import Optional

from collections import namedtuple

from ._python_version_support import (
_flatten_message,
_get_distribution_and_import_packages,
)

from packaging.version import parse as parse_version

# Here we list all the packages for which we want to issue warnings
# about deprecated and unsupported versions.
DependencyConstraint = namedtuple(
"DependencyConstraint",
["package_name", "minimum_fully_supported_version", "recommended_version"],
)
_PACKAGE_DEPENDENCY_WARNINGS = [
DependencyConstraint(
"google.protobuf",
minimum_fully_supported_version="4.25.8",
recommended_version="6.x",
)
]


DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"])
# Version string we provide in a DependencyVersion when we can't determine the version of a
# package.
UNKNOWN_VERSION_STRING = "--"


def get_dependency_version(
dependency_name: str,
) -> DependencyVersion:
"""Get the parsed version of an installed package dependency.

This function checks for an installed package and returns its version
as a `packaging.version.Version` object for safe comparison. It handles
both modern (Python 3.8+) and legacy (Python 3.7) environments.

Args:
dependency_name: The distribution name of the package (e.g., 'requests').

Returns:
A DependencyVersion namedtuple with `version` and
`version_string` attributes, or `DependencyVersion(None,
UNKNOWN_VERSION_STRING)` if the package is not found or
another error occurs during version discovery.

"""
try:
if sys.version_info >= (3, 8):
from importlib import metadata

version_string = metadata.version(dependency_name)
return DependencyVersion(parse_version(version_string), version_string)

# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
# this code path once we drop support for Python 3.7
else: # pragma: NO COVER
# Use pkg_resources, which is part of setuptools.
import pkg_resources

version_string = pkg_resources.get_distribution(dependency_name).version
return DependencyVersion(parse_version(version_string), version_string)

except Exception:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what exceptions to expect? Or does this need to be broad?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any exception related that comes up trying to find the version. I could try to track them down, but at the end of the day I aim for the function to return without an error, but indicating if the version could not be found. So I lean towards catching broadly, but I'm open to counterarguments.

return DependencyVersion(None, UNKNOWN_VERSION_STRING)


def warn_deprecation_for_versions_less_than(
consumer_import_package: str,
dependency_import_package: str,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it might be nice if these variables were more distinct. Maybe target_package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

minimum_fully_supported_version: str,
recommended_version: Optional[str] = None,
message_template: Optional[str] = None,
):
"""Issue any needed deprecation warnings for `dependency_import_package`.

If `dependency_import_package` is installed at a version less than
`minimum_fully_supported_version`, this issues a warning using either a
default `message_template` or one provided by the user. The
default `message_template` informs the user that they will not receive
future updates for `consumer_import_package` if
`dependency_import_package` is somehow pinned to a version lower
than `minimum_fully_supported_version`.

Args:
consumer_import_package: The import name of the package that
needs `dependency_import_package`.
dependency_import_package: The import name of the dependency to check.
minimum_fully_supported_version: The dependency_import_package version number
below which a deprecation warning will be logged.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be minimum_supported_version? Or am I misunderstanding it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

recommended_version: If provided, the recommended next version, which
could be higher than `minimum_fully_supported_version`.
message_template: A custom default message template to replace
the default. This `message_template` is treated as an
f-string, where the following variables are defined:
`dependency_import_package`, `consumer_import_package` and
`dependency_distribution_package` and
`consumer_distribution_package` and `dependency_package`,
`consumer_package` , which contain the import packages, the
distribution packages, and pretty string with both the
distribution and import packages for the dependency and the
consumer, respectively; and `minimum_fully_supported_version`,
`version_used`, and `version_used_string`, which refer to supported
and currently-used versions of the dependency.

"""
if (
not consumer_import_package
or not dependency_import_package
or not minimum_fully_supported_version
): # pragma: NO COVER
return
dependency_version = get_dependency_version(dependency_import_package)
if not dependency_version.version:
return
if dependency_version.version < parse_version(minimum_fully_supported_version):
(
dependency_package,
dependency_distribution_package,
) = _get_distribution_and_import_packages(dependency_import_package)
(
consumer_package,
consumer_distribution_package,
) = _get_distribution_and_import_packages(consumer_import_package)

recommendation = (
" (we recommend {recommended_version})" if recommended_version else ""
)
message_template = message_template or _flatten_message(
"""
DEPRECATION: Package {consumer_package} depends on
{dependency_package}, currently installed at version
{version_used_string}. Future updates to
{consumer_package} will require {dependency_package} at
version {minimum_fully_supported_version} or
higher{recommendation}. Please ensure that either (a) your
Python environment doesn't pin the version of
{dependency_package}, so that updates to
{consumer_package} can require the higher version, or (b)
you manually update your Python environment to use at
least version {minimum_fully_supported_version} of
{dependency_package}.
"""
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this into a constant? Maybe DEFAULT_PACKAGE_DEPRECATION_TEMPLATE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to because the message template is defined and directly used in one place, and it's closely coupled to the variables provided by the function. I think black-boxing it inside the function, and defining it where it's used, is clearer and more compact than defining a separate constant inside or outside the function.

warnings.warn(
message_template.format(
consumer_import_package=consumer_import_package,
dependency_import_package=dependency_import_package,
consumer_distribution_package=consumer_distribution_package,
dependency_distribution_package=dependency_distribution_package,
dependency_package=dependency_package,
consumer_package=consumer_package,
minimum_fully_supported_version=minimum_fully_supported_version,
recommendation=recommendation,
version_used=dependency_version.version,
version_used_string=dependency_version.version_string,
),
FutureWarning,
)


def check_dependency_versions(
consumer_import_package: str, *package_dependency_warnings: DependencyConstraint
):
"""Bundle checks for all package dependencies.

This function can be called by all consumers of google.api_core,
to emit needed deprecation warnings for any of their
dependencies. The dependencies to check can be passed as arguments, or if
none are provided, it will default to the list in
`_PACKAGE_DEPENDENCY_WARNINGS`.

Args:
consumer_import_package: The distribution name of the calling package, whose
dependencies we're checking.
*package_dependency_warnings: A variable number of DependencyConstraint
objects, each specifying a dependency to check.
"""
if not package_dependency_warnings:
package_dependency_warnings = tuple(_PACKAGE_DEPENDENCY_WARNINGS)
for package_info in package_dependency_warnings:
warn_deprecation_for_versions_less_than(
consumer_import_package,
package_info.package_name,
package_info.minimum_fully_supported_version,
recommended_version=package_info.recommended_version,
)
Loading