Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/api_core/_python_package_support.py: 62%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

45 statements  

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, Tuple 

20 

21from collections import namedtuple 

22 

23from ._python_version_support import ( 

24 _flatten_message, 

25 _get_distribution_and_import_packages, 

26) 

27 

28if sys.version_info >= (3, 8): 

29 from importlib import metadata 

30else: 

31 # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove 

32 # this code path once we drop support for Python 3.7 

33 import importlib_metadata as metadata 

34 

35ParsedVersion = Tuple[int, ...] 

36 

37# Here we list all the packages for which we want to issue warnings 

38# about deprecated and unsupported versions. 

39DependencyConstraint = namedtuple( 

40 "DependencyConstraint", 

41 ["package_name", "minimum_fully_supported_version", "recommended_version"], 

42) 

43_PACKAGE_DEPENDENCY_WARNINGS = [ 

44 DependencyConstraint( 

45 "google.protobuf", 

46 minimum_fully_supported_version="4.25.8", 

47 recommended_version="6.x", 

48 ) 

49] 

50 

51 

52DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"]) 

53# Version string we provide in a DependencyVersion when we can't determine the version of a 

54# package. 

55UNKNOWN_VERSION_STRING = "--" 

56 

57 

58def parse_version_to_tuple(version_string: str) -> ParsedVersion: 

59 """Safely converts a semantic version string to a comparable tuple of integers. 

60 

61 Example: "4.25.8" -> (4, 25, 8) 

62 Ignores non-numeric parts and handles common version formats. 

63 

64 Args: 

65 version_string: Version string in the format "x.y.z" or "x.y.z<suffix>" 

66 

67 Returns: 

68 Tuple of integers for the parsed version string. 

69 """ 

70 parts = [] 

71 for part in version_string.split("."): 

72 try: 

73 parts.append(int(part)) 

74 except ValueError: 

75 # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here. 

76 # This is a simplification compared to 'packaging.parse_version', but sufficient 

77 # for comparing strictly numeric semantic versions. 

78 break 

79 return tuple(parts) 

80 

81 

82def get_dependency_version( 

83 dependency_name: str, 

84) -> DependencyVersion: 

85 """Get the parsed version of an installed package dependency. 

86 

87 This function checks for an installed package and returns its version 

88 as a comparable tuple of integers object for safe comparison. It handles 

89 both modern (Python 3.8+) and legacy (Python 3.7) environments. 

90 

91 Args: 

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

93 

94 Returns: 

95 A DependencyVersion namedtuple with `version` (a tuple of integers) and 

96 `version_string` attributes, or `DependencyVersion(None, 

97 UNKNOWN_VERSION_STRING)` if the package is not found or 

98 another error occurs during version discovery. 

99 

100 """ 

101 try: 

102 version_string: str = metadata.version(dependency_name) 

103 parsed_version = parse_version_to_tuple(version_string) 

104 return DependencyVersion(parsed_version, version_string) 

105 except Exception: 

106 # Catch exceptions from metadata.version() (e.g., PackageNotFoundError) 

107 # or errors during parse_version_to_tuple 

108 return DependencyVersion(None, UNKNOWN_VERSION_STRING) 

109 

110 

111def warn_deprecation_for_versions_less_than( 

112 consumer_import_package: str, 

113 dependency_import_package: str, 

114 minimum_fully_supported_version: str, 

115 recommended_version: Optional[str] = None, 

116 message_template: Optional[str] = None, 

117): 

118 """Issue any needed deprecation warnings for `dependency_import_package`. 

119 

120 If `dependency_import_package` is installed at a version less than 

121 `minimum_fully_supported_version`, this issues a warning using either a 

122 default `message_template` or one provided by the user. The 

123 default `message_template` informs the user that they will not receive 

124 future updates for `consumer_import_package` if 

125 `dependency_import_package` is somehow pinned to a version lower 

126 than `minimum_fully_supported_version`. 

127 

128 Args: 

129 consumer_import_package: The import name of the package that 

130 needs `dependency_import_package`. 

131 dependency_import_package: The import name of the dependency to check. 

132 minimum_fully_supported_version: The dependency_import_package version number 

133 below which a deprecation warning will be logged. 

134 recommended_version: If provided, the recommended next version, which 

135 could be higher than `minimum_fully_supported_version`. 

136 message_template: A custom default message template to replace 

137 the default. This `message_template` is treated as an 

138 f-string, where the following variables are defined: 

139 `dependency_import_package`, `consumer_import_package` and 

140 `dependency_distribution_package` and 

141 `consumer_distribution_package` and `dependency_package`, 

142 `consumer_package` , which contain the import packages, the 

143 distribution packages, and pretty string with both the 

144 distribution and import packages for the dependency and the 

145 consumer, respectively; and `minimum_fully_supported_version`, 

146 `version_used`, and `version_used_string`, which refer to supported 

147 and currently-used versions of the dependency. 

148 

149 """ 

150 if ( 

151 not consumer_import_package 

152 or not dependency_import_package 

153 or not minimum_fully_supported_version 

154 ): # pragma: NO COVER 

155 return 

156 

157 dependency_version = get_dependency_version(dependency_import_package) 

158 if not dependency_version.version: 

159 return 

160 

161 if dependency_version.version < parse_version_to_tuple( 

162 minimum_fully_supported_version 

163 ): 

164 ( 

165 dependency_package, 

166 dependency_distribution_package, 

167 ) = _get_distribution_and_import_packages(dependency_import_package) 

168 ( 

169 consumer_package, 

170 consumer_distribution_package, 

171 ) = _get_distribution_and_import_packages(consumer_import_package) 

172 

173 recommendation = ( 

174 " (we recommend {recommended_version})" if recommended_version else "" 

175 ) 

176 message_template = message_template or _flatten_message( 

177 """ 

178 DEPRECATION: Package {consumer_package} depends on 

179 {dependency_package}, currently installed at version 

180 {version_used_string}. Future updates to 

181 {consumer_package} will require {dependency_package} at 

182 version {minimum_fully_supported_version} or 

183 higher{recommendation}. Please ensure that either (a) your 

184 Python environment doesn't pin the version of 

185 {dependency_package}, so that updates to 

186 {consumer_package} can require the higher version, or (b) 

187 you manually update your Python environment to use at 

188 least version {minimum_fully_supported_version} of 

189 {dependency_package}. 

190 """ 

191 ) 

192 warnings.warn( 

193 message_template.format( 

194 consumer_import_package=consumer_import_package, 

195 dependency_import_package=dependency_import_package, 

196 consumer_distribution_package=consumer_distribution_package, 

197 dependency_distribution_package=dependency_distribution_package, 

198 dependency_package=dependency_package, 

199 consumer_package=consumer_package, 

200 minimum_fully_supported_version=minimum_fully_supported_version, 

201 recommendation=recommendation, 

202 version_used=dependency_version.version, 

203 version_used_string=dependency_version.version_string, 

204 ), 

205 FutureWarning, 

206 ) 

207 

208 

209def check_dependency_versions( 

210 consumer_import_package: str, *package_dependency_warnings: DependencyConstraint 

211): 

212 """Bundle checks for all package dependencies. 

213 

214 This function can be called by all consumers of google.api_core, 

215 to emit needed deprecation warnings for any of their 

216 dependencies. The dependencies to check can be passed as arguments, or if 

217 none are provided, it will default to the list in 

218 `_PACKAGE_DEPENDENCY_WARNINGS`. 

219 

220 Args: 

221 consumer_import_package: The distribution name of the calling package, whose 

222 dependencies we're checking. 

223 *package_dependency_warnings: A variable number of DependencyConstraint 

224 objects, each specifying a dependency to check. 

225 """ 

226 if not package_dependency_warnings: 

227 package_dependency_warnings = tuple(_PACKAGE_DEPENDENCY_WARNINGS) 

228 for package_info in package_dependency_warnings: 

229 warn_deprecation_for_versions_less_than( 

230 consumer_import_package, 

231 package_info.package_name, 

232 package_info.minimum_fully_supported_version, 

233 recommended_version=package_info.recommended_version, 

234 )