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 

20import logging 

21import sys 

22import textwrap 

23import warnings 

24from importlib import metadata 

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

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

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

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

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

79 ), 

80 VersionInfo( 

81 version="3.11", 

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

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

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

85 ), 

86 VersionInfo( 

87 version="3.12", 

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

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

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

91 ), 

92 VersionInfo( 

93 version="3.13", 

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

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

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

97 ), 

98 VersionInfo( 

99 version="3.14", 

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

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

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

103 ), 

104] 

105 

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

107for info in PYTHON_VERSIONS: 

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

109 PYTHON_VERSION_INFO[(major, minor)] = info 

110 

111 

112LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

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

114_FAKE_PAST_VERSION = VersionInfo( 

115 version="0.0", 

116 python_beta=_FAKE_PAST_DATE, 

117 python_start=_FAKE_PAST_DATE, 

118 python_eol=_FAKE_PAST_DATE, 

119) 

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

121_FAKE_FUTURE_VERSION = VersionInfo( 

122 version="999.0", 

123 python_beta=_FAKE_FUTURE_DATE, 

124 python_start=_FAKE_FUTURE_DATE, 

125 python_eol=_FAKE_FUTURE_DATE, 

126) 

127DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) 

128EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

129 

130 

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

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

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

134 

135 

136@functools.cache 

137def _cached_packages_distributions(): 

138 return metadata.packages_distributions() 

139 

140 

141def _get_pypi_package_name(module_name): 

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

143 try: 

144 module_to_distributions = _cached_packages_distributions() 

145 

146 if module_name in module_to_distributions: # pragma: NO COVER 

147 return module_to_distributions[module_name][0] 

148 except Exception as e: # pragma: NO COVER 

149 _LOGGER.info( 

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

151 module_name, 

152 e, 

153 ) 

154 

155 return None 

156 

157 

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

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

160 distribution_package = _get_pypi_package_name(import_package) 

161 dependency_distribution_and_import_packages = ( 

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

163 if distribution_package 

164 else import_package 

165 ) 

166 return dependency_distribution_and_import_packages, distribution_package 

167 

168 

169def check_python_version( 

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

171) -> PythonVersionStatus: 

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

173 

174 Args: 

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

176 

177 Returns: 

178 The support status of the current Python version. 

179 """ 

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

181 

182 python_version = sys.version_info 

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

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

185 

186 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

187 

188 if not version_info: 

189 if version_tuple < LOWEST_TRACKED_VERSION: 

190 version_info = _FAKE_PAST_VERSION 

191 else: 

192 version_info = _FAKE_FUTURE_VERSION 

193 

194 gapic_deprecation = version_info.gapic_deprecation or ( 

195 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

196 ) 

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

198 

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

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

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

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

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

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

205 

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

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

208 def get_package_label(): 

209 label, _ = _get_distribution_and_import_packages(package) 

210 return label 

211 

212 if gapic_end < today: 

213 package_label = get_package_label() 

214 message = _flatten_message( 

215 f""" 

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

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

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

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

220 {package_label}. 

221 """ 

222 ) 

223 warnings.warn(message, FutureWarning) 

224 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

225 

226 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

227 if eol_date <= today <= gapic_end: 

228 package_label = get_package_label() 

229 message = _flatten_message( 

230 f""" 

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

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

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

234 with any other fixes or features. Please upgrade 

235 to the latest Python version, or at least Python 

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

237 """ 

238 ) 

239 warnings.warn(message, FutureWarning) 

240 return PythonVersionStatus.PYTHON_VERSION_EOL 

241 

242 if gapic_deprecation <= today <= gapic_end: 

243 package_label = get_package_label() 

244 message = _flatten_message( 

245 f""" 

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

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

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

249 latest Python version, or at least Python 

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

251 for {package_label} past that date. 

252 """ 

253 ) 

254 warnings.warn(message, FutureWarning) 

255 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

256 

257 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED