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

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

86 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 warnings 

20import sys 

21import textwrap 

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

23 

24 

25class PythonVersionStatus(enum.Enum): 

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

27 

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

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

30 Python. 

31 """ 

32 

33 PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED" 

34 

35 PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED" 

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

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

38 

39 PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED" 

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

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

42 running under this Python version.""" 

43 

44 PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL" 

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

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

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

48 

49 PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED" 

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

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

52 

53 

54class VersionInfo(NamedTuple): 

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

56 

57 version: str 

58 python_beta: Optional[datetime.date] 

59 python_start: datetime.date 

60 python_eol: datetime.date 

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

62 gapic_deprecation: Optional[datetime.date] = None 

63 gapic_end: Optional[datetime.date] = None 

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

65 

66 

67PYTHON_VERSIONS: List[VersionInfo] = [ 

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

69 VersionInfo( 

70 version="3.7", 

71 python_beta=None, 

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

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

74 ), 

75 VersionInfo( 

76 version="3.8", 

77 python_beta=None, 

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

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

80 ), 

81 VersionInfo( 

82 version="3.9", 

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

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

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

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

87 ), 

88 VersionInfo( 

89 version="3.10", 

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

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

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

93 ), 

94 VersionInfo( 

95 version="3.11", 

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

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

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

99 ), 

100 VersionInfo( 

101 version="3.12", 

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

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

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

105 ), 

106 VersionInfo( 

107 version="3.13", 

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

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

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

111 ), 

112 VersionInfo( 

113 version="3.14", 

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

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

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

117 ), 

118] 

119 

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

121for info in PYTHON_VERSIONS: 

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

123 PYTHON_VERSION_INFO[(major, minor)] = info 

124 

125 

126LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

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

128_FAKE_PAST_VERSION = VersionInfo( 

129 version="0.0", 

130 python_beta=_FAKE_PAST_DATE, 

131 python_start=_FAKE_PAST_DATE, 

132 python_eol=_FAKE_PAST_DATE, 

133) 

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

135_FAKE_FUTURE_VERSION = VersionInfo( 

136 version="999.0", 

137 python_beta=_FAKE_FUTURE_DATE, 

138 python_start=_FAKE_FUTURE_DATE, 

139 python_eol=_FAKE_FUTURE_DATE, 

140) 

141DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) 

142EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

143 

144 

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

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

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

148 

149 

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

151# no longer support Python 3.7 

152if sys.version_info < (3, 8): 

153 

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

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

156 return None 

157 

158else: 

159 from importlib import metadata 

160 

161 def _get_pypi_package_name(module_name): 

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

163 try: 

164 # Get the mapping of modules to distributions 

165 module_to_distributions = metadata.packages_distributions() 

166 

167 # Check if the module is found in the mapping 

168 if module_name in module_to_distributions: # pragma: NO COVER 

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

170 return module_to_distributions[module_name][0] 

171 else: 

172 return None # Module not found in the mapping 

173 except Exception as e: 

174 print(f"An error occurred: {e}") 

175 return None 

176 

177 

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

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

180 distribution_package = _get_pypi_package_name(import_package) 

181 dependency_distribution_and_import_packages = ( 

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

183 if distribution_package 

184 else import_package 

185 ) 

186 return dependency_distribution_and_import_packages, distribution_package 

187 

188 

189def check_python_version( 

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

191) -> PythonVersionStatus: 

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

193 

194 Args: 

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

196 

197 Returns: 

198 The support status of the current Python version. 

199 """ 

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

201 package_label, _ = _get_distribution_and_import_packages(package) 

202 

203 python_version = sys.version_info 

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

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

206 

207 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

208 

209 if not version_info: 

210 if version_tuple < LOWEST_TRACKED_VERSION: 

211 version_info = _FAKE_PAST_VERSION 

212 else: 

213 version_info = _FAKE_FUTURE_VERSION 

214 

215 gapic_deprecation = version_info.gapic_deprecation or ( 

216 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

217 ) 

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

219 

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

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

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

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

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

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

226 

227 if gapic_end < today: 

228 message = _flatten_message( 

229 f""" 

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

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

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

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

234 {package_label}. 

235 """ 

236 ) 

237 warnings.warn(message, FutureWarning) 

238 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

239 

240 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

241 if eol_date <= today <= gapic_end: 

242 message = _flatten_message( 

243 f""" 

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

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

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

247 with any other fixes or features. Please upgrade 

248 to the latest Python version, or at least Python 

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

250 """ 

251 ) 

252 warnings.warn(message, FutureWarning) 

253 return PythonVersionStatus.PYTHON_VERSION_EOL 

254 

255 if gapic_deprecation <= today <= gapic_end: 

256 message = _flatten_message( 

257 f""" 

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

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

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

261 latest Python version, or at least Python 

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

263 for {package_label} past that date. 

264 """ 

265 ) 

266 warnings.warn(message, FutureWarning) 

267 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

268 

269 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED