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

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

85 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 logging 

20import warnings 

21import sys 

22import textwrap 

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

24 

25 

26_LOGGER = logging.getLogger(__name__) 

27 

28 

29class PythonVersionStatus(enum.Enum): 

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

31 

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

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

34 Python. 

35 """ 

36 

37 PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED" 

38 

39 PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED" 

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

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

42 

43 PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED" 

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

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

46 running under this Python version.""" 

47 

48 PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL" 

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

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

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

52 

53 PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED" 

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

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

56 

57 

58class VersionInfo(NamedTuple): 

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

60 

61 version: str 

62 python_beta: Optional[datetime.date] 

63 python_start: datetime.date 

64 python_eol: datetime.date 

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

66 gapic_deprecation: Optional[datetime.date] = None 

67 gapic_end: Optional[datetime.date] = None 

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

69 

70 

71PYTHON_VERSIONS: List[VersionInfo] = [ 

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

73 VersionInfo( 

74 version="3.7", 

75 python_beta=None, 

76 python_start=datetime.date(2018, 6, 27), 

77 python_eol=datetime.date(2023, 6, 27), 

78 ), 

79 VersionInfo( 

80 version="3.8", 

81 python_beta=None, 

82 python_start=datetime.date(2019, 10, 14), 

83 python_eol=datetime.date(2024, 10, 7), 

84 ), 

85 VersionInfo( 

86 version="3.9", 

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

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

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

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

91 ), 

92 VersionInfo( 

93 version="3.10", 

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

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

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

97 ), 

98 VersionInfo( 

99 version="3.11", 

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

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

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

103 ), 

104 VersionInfo( 

105 version="3.12", 

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

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

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

109 ), 

110 VersionInfo( 

111 version="3.13", 

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

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

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

115 ), 

116 VersionInfo( 

117 version="3.14", 

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

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

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

121 ), 

122] 

123 

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

125for info in PYTHON_VERSIONS: 

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

127 PYTHON_VERSION_INFO[(major, minor)] = info 

128 

129 

130LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

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

132_FAKE_PAST_VERSION = VersionInfo( 

133 version="0.0", 

134 python_beta=_FAKE_PAST_DATE, 

135 python_start=_FAKE_PAST_DATE, 

136 python_eol=_FAKE_PAST_DATE, 

137) 

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

139_FAKE_FUTURE_VERSION = VersionInfo( 

140 version="999.0", 

141 python_beta=_FAKE_FUTURE_DATE, 

142 python_start=_FAKE_FUTURE_DATE, 

143 python_eol=_FAKE_FUTURE_DATE, 

144) 

145DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) 

146EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

147 

148 

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

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

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

152 

153 

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

155# Remove once we no longer support Python 3.9. 

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

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

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

159 

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

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

162 return None 

163 

164else: 

165 from importlib import metadata 

166 

167 def _get_pypi_package_name(module_name): 

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

169 try: 

170 # Get the mapping of modules to distributions 

171 module_to_distributions = metadata.packages_distributions() 

172 

173 # Check if the module is found in the mapping 

174 if module_name in module_to_distributions: # pragma: NO COVER 

175 # The value is a list of distribution names, take the first one 

176 return module_to_distributions[module_name][0] 

177 except Exception as e: # pragma: NO COVER 

178 _LOGGER.info( 

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

180 module_name, 

181 e, 

182 ) 

183 

184 return None 

185 

186 

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

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

189 distribution_package = _get_pypi_package_name(import_package) 

190 dependency_distribution_and_import_packages = ( 

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

192 if distribution_package 

193 else import_package 

194 ) 

195 return dependency_distribution_and_import_packages, distribution_package 

196 

197 

198def check_python_version( 

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

200) -> PythonVersionStatus: 

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

202 

203 Args: 

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

205 

206 Returns: 

207 The support status of the current Python version. 

208 """ 

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

210 package_label, _ = _get_distribution_and_import_packages(package) 

211 

212 python_version = sys.version_info 

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

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

215 

216 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

217 

218 if not version_info: 

219 if version_tuple < LOWEST_TRACKED_VERSION: 

220 version_info = _FAKE_PAST_VERSION 

221 else: 

222 version_info = _FAKE_FUTURE_VERSION 

223 

224 gapic_deprecation = version_info.gapic_deprecation or ( 

225 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

226 ) 

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

228 

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

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

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

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

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

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

235 

236 if gapic_end < today: 

237 message = _flatten_message( 

238 f""" 

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

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

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

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

243 {package_label}. 

244 """ 

245 ) 

246 warnings.warn(message, FutureWarning) 

247 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

248 

249 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

250 if eol_date <= today <= gapic_end: 

251 message = _flatten_message( 

252 f""" 

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

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

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

256 with any other fixes or features. Please upgrade 

257 to the latest Python version, or at least Python 

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

259 """ 

260 ) 

261 warnings.warn(message, FutureWarning) 

262 return PythonVersionStatus.PYTHON_VERSION_EOL 

263 

264 if gapic_deprecation <= today <= gapic_end: 

265 message = _flatten_message( 

266 f""" 

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

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

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

270 latest Python version, or at least Python 

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

272 for {package_label} past that date. 

273 """ 

274 ) 

275 warnings.warn(message, FutureWarning) 

276 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

277 

278 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED