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.9", 

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

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

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

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

79 ), 

80 VersionInfo( 

81 version="3.10", 

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

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

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

85 ), 

86 VersionInfo( 

87 version="3.11", 

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

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

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

91 ), 

92 VersionInfo( 

93 version="3.12", 

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

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

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

97 ), 

98 VersionInfo( 

99 version="3.13", 

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

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

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

103 ), 

104 VersionInfo( 

105 version="3.14", 

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

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

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

109 ), 

110] 

111 

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

113for info in PYTHON_VERSIONS: 

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

115 PYTHON_VERSION_INFO[(major, minor)] = info 

116 

117 

118LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

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

120_FAKE_PAST_VERSION = VersionInfo( 

121 version="0.0", 

122 python_beta=_FAKE_PAST_DATE, 

123 python_start=_FAKE_PAST_DATE, 

124 python_eol=_FAKE_PAST_DATE, 

125) 

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

127_FAKE_FUTURE_VERSION = VersionInfo( 

128 version="999.0", 

129 python_beta=_FAKE_FUTURE_DATE, 

130 python_start=_FAKE_FUTURE_DATE, 

131 python_eol=_FAKE_FUTURE_DATE, 

132) 

133DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) 

134EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

135 

136 

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

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

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

140 

141 

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

143# Remove once we no longer support Python 3.9. 

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

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

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

147 

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

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

150 return None 

151 

152else: 

153 from importlib import metadata 

154 

155 def _get_pypi_package_name(module_name): 

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

157 try: 

158 # Get the mapping of modules to distributions 

159 module_to_distributions = metadata.packages_distributions() 

160 

161 # Check if the module is found in the mapping 

162 if module_name in module_to_distributions: # pragma: NO COVER 

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

164 return module_to_distributions[module_name][0] 

165 except Exception as e: # pragma: NO COVER 

166 _LOGGER.info( 

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

168 module_name, 

169 e, 

170 ) 

171 

172 return None 

173 

174 

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

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

177 distribution_package = _get_pypi_package_name(import_package) 

178 dependency_distribution_and_import_packages = ( 

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

180 if distribution_package 

181 else import_package 

182 ) 

183 return dependency_distribution_and_import_packages, distribution_package 

184 

185 

186def check_python_version( 

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

188) -> PythonVersionStatus: 

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

190 

191 Args: 

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

193 

194 Returns: 

195 The support status of the current Python version. 

196 """ 

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

198 package_label, _ = _get_distribution_and_import_packages(package) 

199 

200 python_version = sys.version_info 

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

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

203 

204 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

205 

206 if not version_info: 

207 if version_tuple < LOWEST_TRACKED_VERSION: 

208 version_info = _FAKE_PAST_VERSION 

209 else: 

210 version_info = _FAKE_FUTURE_VERSION 

211 

212 gapic_deprecation = version_info.gapic_deprecation or ( 

213 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

214 ) 

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

216 

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

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

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

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

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

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

223 

224 if gapic_end < today: 

225 message = _flatten_message( 

226 f""" 

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

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

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

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

231 {package_label}. 

232 """ 

233 ) 

234 warnings.warn(message, FutureWarning) 

235 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

236 

237 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

238 if eol_date <= today <= gapic_end: 

239 message = _flatten_message( 

240 f""" 

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

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

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

244 with any other fixes or features. Please upgrade 

245 to the latest Python version, or at least Python 

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

247 """ 

248 ) 

249 warnings.warn(message, FutureWarning) 

250 return PythonVersionStatus.PYTHON_VERSION_EOL 

251 

252 if gapic_deprecation <= today <= gapic_end: 

253 message = _flatten_message( 

254 f""" 

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

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

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

258 latest Python version, or at least Python 

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

260 for {package_label} past that date. 

261 """ 

262 ) 

263 warnings.warn(message, FutureWarning) 

264 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

265 

266 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED