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        )