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

93 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 

20from importlib import metadata 

21import logging 

22import warnings 

23import sys 

24import textwrap 

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

26 

27 

28_LOGGER = logging.getLogger(__name__) 

29 

30 

31class PythonVersionStatus(enum.Enum): 

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

33 

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

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

36 Python. 

37 """ 

38 

39 PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED" 

40 

41 PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED" 

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

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

44 

45 PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED" 

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

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

48 running under this Python version.""" 

49 

50 PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL" 

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

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

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

54 

55 PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED" 

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

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

58 

59 

60class VersionInfo(NamedTuple): 

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

62 

63 version: str 

64 python_beta: Optional[datetime.date] 

65 python_start: datetime.date 

66 python_eol: datetime.date 

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

68 gapic_deprecation: Optional[datetime.date] = None 

69 gapic_end: Optional[datetime.date] = None 

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

71 

72 

73PYTHON_VERSIONS: List[VersionInfo] = [ 

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

75 VersionInfo( 

76 version="3.10", 

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

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

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

80 ), 

81 VersionInfo( 

82 version="3.11", 

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

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

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

86 ), 

87 VersionInfo( 

88 version="3.12", 

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

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

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

92 ), 

93 VersionInfo( 

94 version="3.13", 

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

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

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

98 ), 

99 VersionInfo( 

100 version="3.14", 

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

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

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

104 ), 

105] 

106 

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

108for info in PYTHON_VERSIONS: 

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

110 PYTHON_VERSION_INFO[(major, minor)] = info 

111 

112 

113LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

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

115_FAKE_PAST_VERSION = VersionInfo( 

116 version="0.0", 

117 python_beta=_FAKE_PAST_DATE, 

118 python_start=_FAKE_PAST_DATE, 

119 python_eol=_FAKE_PAST_DATE, 

120) 

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

122_FAKE_FUTURE_VERSION = VersionInfo( 

123 version="999.0", 

124 python_beta=_FAKE_FUTURE_DATE, 

125 python_start=_FAKE_FUTURE_DATE, 

126 python_eol=_FAKE_FUTURE_DATE, 

127) 

128DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) 

129EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

130 

131 

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

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

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

135 

136 

137@functools.cache 

138def _cached_packages_distributions(): 

139 return metadata.packages_distributions() 

140 

141 

142def _get_pypi_package_name(module_name): 

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

144 try: 

145 module_to_distributions = _cached_packages_distributions() 

146 

147 if module_name in module_to_distributions: # pragma: NO COVER 

148 return module_to_distributions[module_name][0] 

149 except Exception as e: # pragma: NO COVER 

150 _LOGGER.info( 

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

152 module_name, 

153 e, 

154 ) 

155 

156 return None 

157 

158 

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

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

161 distribution_package = _get_pypi_package_name(import_package) 

162 dependency_distribution_and_import_packages = ( 

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

164 if distribution_package 

165 else import_package 

166 ) 

167 return dependency_distribution_and_import_packages, distribution_package 

168 

169 

170def check_python_version( 

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

172) -> PythonVersionStatus: 

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

174 

175 Args: 

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

177 

178 Returns: 

179 The support status of the current Python version. 

180 """ 

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

182 

183 python_version = sys.version_info 

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

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

186 

187 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

188 

189 if not version_info: 

190 if version_tuple < LOWEST_TRACKED_VERSION: 

191 version_info = _FAKE_PAST_VERSION 

192 else: 

193 version_info = _FAKE_FUTURE_VERSION 

194 

195 gapic_deprecation = version_info.gapic_deprecation or ( 

196 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

197 ) 

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

199 

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

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

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

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

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

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

206 

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

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

209 def get_package_label(): 

210 label, _ = _get_distribution_and_import_packages(package) 

211 return label 

212 

213 if gapic_end < today: 

214 package_label = get_package_label() 

215 message = _flatten_message( 

216 f""" 

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

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

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

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

221 {package_label}. 

222 """ 

223 ) 

224 warnings.warn(message, FutureWarning) 

225 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

226 

227 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

228 if eol_date <= today <= gapic_end: 

229 package_label = get_package_label() 

230 message = _flatten_message( 

231 f""" 

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

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

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

235 with any other fixes or features. Please upgrade 

236 to the latest Python version, or at least Python 

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

238 """ 

239 ) 

240 warnings.warn(message, FutureWarning) 

241 return PythonVersionStatus.PYTHON_VERSION_EOL 

242 

243 if gapic_deprecation <= today <= gapic_end: 

244 package_label = get_package_label() 

245 message = _flatten_message( 

246 f""" 

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

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

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

250 latest Python version, or at least Python 

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

252 for {package_label} past that date. 

253 """ 

254 ) 

255 warnings.warn(message, FutureWarning) 

256 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

257 

258 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED