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

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

94 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 Python versions supported by Google Cloud Client Libraries.""" 

16 

17import datetime 

18import enum 

19import functools 

20import logging 

21import warnings 

22import sys 

23import textwrap 

24from typing import Any, List, NamedTuple, Optional, Dict, Tuple 

25 

26 

27_LOGGER = logging.getLogger(__name__) 

28 

29 

30class PythonVersionStatus(enum.Enum): 

31 """Support status of a Python version in this client library artifact release. 

32 

33 "Support", in this context, means that this release of a client library 

34 artifact is configured to run on the currently configured version of 

35 Python. 

36 """ 

37 

38 PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED" 

39 

40 PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED" 

41 """This Python version is fully supported, so the artifact running on this 

42 version will have all features and bug fixes.""" 

43 

44 PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED" 

45 """This Python version is still supported, but support will end within a 

46 year. At that time, there will be no more releases for this artifact 

47 running under this Python version.""" 

48 

49 PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL" 

50 """This Python version has reached its end of life in the Python community 

51 (see https://devguide.python.org/versions/), and this artifact will cease 

52 supporting this Python version within the next few releases.""" 

53 

54 PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED" 

55 """This release of the client library artifact may not be the latest, since 

56 current releases no longer support this Python version.""" 

57 

58 

59class VersionInfo(NamedTuple): 

60 """Hold release and support date information for a Python version.""" 

61 

62 version: str 

63 python_beta: Optional[datetime.date] 

64 python_start: datetime.date 

65 python_eol: datetime.date 

66 gapic_start: Optional[datetime.date] = None # unused 

67 gapic_deprecation: Optional[datetime.date] = None 

68 gapic_end: Optional[datetime.date] = None 

69 dep_unpatchable_cve: Optional[datetime.date] = None # unused 

70 

71 

72PYTHON_VERSIONS: List[VersionInfo] = [ 

73 # Refer to https://devguide.python.org/versions/ and the PEPs linked therefrom. 

74 VersionInfo( 

75 version="3.9", 

76 python_beta=datetime.date(2020, 5, 18), 

77 python_start=datetime.date(2020, 10, 5), 

78 python_eol=datetime.date(2025, 10, 5), 

79 gapic_end=datetime.date(2025, 10, 5) + datetime.timedelta(days=90), 

80 ), 

81 VersionInfo( 

82 version="3.10", 

83 python_beta=datetime.date(2021, 5, 3), 

84 python_start=datetime.date(2021, 10, 4), 

85 python_eol=datetime.date(2026, 10, 4), # TODO: specify day when announced 

86 ), 

87 VersionInfo( 

88 version="3.11", 

89 python_beta=datetime.date(2022, 5, 8), 

90 python_start=datetime.date(2022, 10, 24), 

91 python_eol=datetime.date(2027, 10, 24), # TODO: specify day when announced 

92 ), 

93 VersionInfo( 

94 version="3.12", 

95 python_beta=datetime.date(2023, 5, 22), 

96 python_start=datetime.date(2023, 10, 2), 

97 python_eol=datetime.date(2028, 10, 2), # TODO: specify day when announced 

98 ), 

99 VersionInfo( 

100 version="3.13", 

101 python_beta=datetime.date(2024, 5, 8), 

102 python_start=datetime.date(2024, 10, 7), 

103 python_eol=datetime.date(2029, 10, 7), # TODO: specify day when announced 

104 ), 

105 VersionInfo( 

106 version="3.14", 

107 python_beta=datetime.date(2025, 5, 7), 

108 python_start=datetime.date(2025, 10, 7), 

109 python_eol=datetime.date(2030, 10, 7), # TODO: specify day when announced 

110 ), 

111] 

112 

113PYTHON_VERSION_INFO: Dict[Tuple[int, int], VersionInfo] = {} 

114for info in PYTHON_VERSIONS: 

115 major, minor = map(int, info.version.split(".")) 

116 PYTHON_VERSION_INFO[(major, minor)] = info 

117 

118 

119LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

120_FAKE_PAST_DATE = datetime.date.min + datetime.timedelta(days=900) 

121_FAKE_PAST_VERSION = VersionInfo( 

122 version="0.0", 

123 python_beta=_FAKE_PAST_DATE, 

124 python_start=_FAKE_PAST_DATE, 

125 python_eol=_FAKE_PAST_DATE, 

126) 

127_FAKE_FUTURE_DATE = datetime.date.max - datetime.timedelta(days=900) 

128_FAKE_FUTURE_VERSION = VersionInfo( 

129 version="999.0", 

130 python_beta=_FAKE_FUTURE_DATE, 

131 python_start=_FAKE_FUTURE_DATE, 

132 python_eol=_FAKE_FUTURE_DATE, 

133) 

134DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) 

135EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

136 

137 

138def _flatten_message(text: str) -> str: 

139 """Dedent a multi-line string and flatten it into a single line.""" 

140 return " ".join(textwrap.dedent(text).strip().split()) 

141 

142 

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

144# Remove once we no longer support Python 3.9. 

145# `importlib.metadata.packages_distributions()` is only supported in Python 3.10 and newer 

146# https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.packages_distributions 

147if sys.version_info < (3, 10): 

148 

149 def _get_pypi_package_name(module_name): # pragma: NO COVER 

150 """Determine the PyPI package name for a given module name.""" 

151 return None 

152 

153else: 

154 from importlib import metadata 

155 

156 @functools.cache 

157 def _cached_packages_distributions(): 

158 return metadata.packages_distributions() 

159 

160 def _get_pypi_package_name(module_name): 

161 """Determine the PyPI package name for a given module name.""" 

162 try: 

163 module_to_distributions = _cached_packages_distributions() 

164 

165 if module_name in module_to_distributions: # pragma: NO COVER 

166 return module_to_distributions[module_name][0] 

167 except Exception as e: # pragma: NO COVER 

168 _LOGGER.info( 

169 "An error occurred while determining PyPI package name for %s: %s", 

170 module_name, 

171 e, 

172 ) 

173 

174 return None 

175 

176 

177def _get_distribution_and_import_packages(import_package: str) -> Tuple[str, Any]: 

178 """Return a pretty string with distribution & import package names.""" 

179 distribution_package = _get_pypi_package_name(import_package) 

180 dependency_distribution_and_import_packages = ( 

181 f"package {distribution_package} ({import_package})" 

182 if distribution_package 

183 else import_package 

184 ) 

185 return dependency_distribution_and_import_packages, distribution_package 

186 

187 

188def check_python_version( 

189 package: str = "this package", today: Optional[datetime.date] = None 

190) -> PythonVersionStatus: 

191 """Check the running Python version and issue a support warning if needed. 

192 

193 Args: 

194 today: The date to check against. Defaults to the current date. 

195 

196 Returns: 

197 The support status of the current Python version. 

198 """ 

199 today = today or datetime.date.today() 

200 

201 python_version = sys.version_info 

202 version_tuple = (python_version.major, python_version.minor) 

203 py_version_str = sys.version.split()[0] 

204 

205 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

206 

207 if not version_info: 

208 if version_tuple < LOWEST_TRACKED_VERSION: 

209 version_info = _FAKE_PAST_VERSION 

210 else: 

211 version_info = _FAKE_FUTURE_VERSION 

212 

213 gapic_deprecation = version_info.gapic_deprecation or ( 

214 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

215 ) 

216 gapic_end = version_info.gapic_end or (version_info.python_eol + EOL_GRACE_PERIOD) 

217 

218 def min_python(date: datetime.date) -> str: 

219 """Find the minimum supported Python version for a given date.""" 

220 for version, info in sorted(PYTHON_VERSION_INFO.items()): 

221 if info.python_start <= date < info.python_eol: 

222 return f"{version[0]}.{version[1]}" 

223 return "at a currently supported version [https://devguide.python.org/versions]" 

224 

225 # Resolve the pretty package label lazily so we avoid any work on 

226 # the happy path (supported Python version, no warning needed). 

227 def get_package_label(): 

228 label, _ = _get_distribution_and_import_packages(package) 

229 return label 

230 

231 if gapic_end < today: 

232 package_label = get_package_label() 

233 message = _flatten_message( 

234 f""" 

235 You are using a non-supported Python version ({py_version_str}). 

236 Google will not post any further updates to {package_label} 

237 supporting this Python version. Please upgrade to the latest Python 

238 version, or at least Python {min_python(today)}, and then update 

239 {package_label}. 

240 """ 

241 ) 

242 warnings.warn(message, FutureWarning) 

243 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

244 

245 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

246 if eol_date <= today <= gapic_end: 

247 package_label = get_package_label() 

248 message = _flatten_message( 

249 f""" 

250 You are using a Python version ({py_version_str}) 

251 past its end of life. Google will update {package_label} 

252 with critical bug fixes on a best-effort basis, but not 

253 with any other fixes or features. Please upgrade 

254 to the latest Python version, or at least Python 

255 {min_python(today)}, and then update {package_label}. 

256 """ 

257 ) 

258 warnings.warn(message, FutureWarning) 

259 return PythonVersionStatus.PYTHON_VERSION_EOL 

260 

261 if gapic_deprecation <= today <= gapic_end: 

262 package_label = get_package_label() 

263 message = _flatten_message( 

264 f""" 

265 You are using a Python version ({py_version_str}) which Google will 

266 stop supporting in new releases of {package_label} once it reaches 

267 its end of life ({version_info.python_eol}). Please upgrade to the 

268 latest Python version, or at least Python 

269 {min_python(version_info.python_eol)}, to continue receiving updates 

270 for {package_label} past that date. 

271 """ 

272 ) 

273 warnings.warn(message, FutureWarning) 

274 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

275 

276 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED