Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 96
feat: provide and use Python version support check#832
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
6316d70a136ad745cd64725225fa1d567c0f1dbbb4e3fd56f8b1dfb14b9208e5cf86527d8b1c7cda8e27db92fac118a7c8f1c46aa474ec0d2083b494ea124ec5949c46fc14738829e20bdb6260789666454a56112e2990b35a2074cd64832e36414ab6245ca3a897ba814ea638dee04b95f4777a34ec15a04e2e68b3f33729896cfd421f98ac76f7ae093a96187f848b575a16ecb72113c587cffbe0f0b28b0b3221c9aece24c13ddbc3a26d472bae0550f83491b695633838905bcf6ffc224b7a6c40ced0414b2835df53File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that this package is missing defining packaging as a explicit dependency. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also just now experiencing an error on this: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that there is already an issue created #848 | ||
| # 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: | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I think that makes sense to be extra cautious here | ||
| return DependencyVersion(None, UNKNOWN_VERSION_STRING) | ||
| def warn_deprecation_for_versions_less_than( | ||
| consumer_import_package: str, | ||
| dependency_import_package: str, | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it might be nice if these variables were more distinct. Maybe ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this be minimum_supported_version? Or am I misunderstanding it? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}. | ||
| """ | ||
| ) | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make this into a constant? Maybe DEFAULT_PACKAGE_DEPRECATION_TEMPLATE? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are both
check_dependency_versionsandwarn_deprecation_for_versions_less_thanmeant to be used externally? I'm just seeingcheck_dependency_versionsin the generator PRThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check_dependency_versionscallswarn_deprecation_for_versions_less_than, so for package deprecation warnings that apply to anything used byapi_coreas well as its dependents, such as the GAPICs, calling the former is sufficient. At the moment, the only such package isprotobuf.warn_deprecation_for_versions_less_thandirectly for packages that they themselves want to issue deprecation warnings for, but which are not used byapi_coreitself. We are not configuring any right now.For those two reasons,
warn_deprecation_for_versions_less_thanat the moment, andwarn_deprecation_for_versions_less_thandoes not have an underscore prefix.I think this design makes sense, but please do check my thinking (and my explanation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think that makes sense, just wanted to make sure I understood the intention