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

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 logging 

20import sys 

21import textwrap 

22import warnings 

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

24 

25_LOGGER = logging.getLogger(__name__) 

26 

27 

28class PythonVersionStatus(enum.Enum): 

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

30 

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

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

33 Python. 

34 """ 

35 

36 PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED" 

37 

38 PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED" 

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

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

41 

42 PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED" 

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

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

45 running under this Python version.""" 

46 

47 PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL" 

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

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

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

51 

52 PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED" 

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

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

55 

56 

57class VersionInfo(NamedTuple): 

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

59 

60 version: str 

61 python_beta: Optional[datetime.date] 

62 python_start: datetime.date 

63 python_eol: datetime.date 

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

65 gapic_deprecation: Optional[datetime.date] = None 

66 gapic_end: Optional[datetime.date] = None 

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

68 

69 

70PYTHON_VERSIONS: List[VersionInfo] = [ 

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

72 VersionInfo( 

73 version="3.10", 

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

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

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

77 ), 

78 VersionInfo( 

79 version="3.11", 

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

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

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

83 ), 

84 VersionInfo( 

85 version="3.12", 

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

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

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

89 ), 

90 VersionInfo( 

91 version="3.13", 

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

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

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

95 ), 

96 VersionInfo( 

97 version="3.14", 

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

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

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

101 ), 

102] 

103 

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

105for info in PYTHON_VERSIONS: 

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

107 PYTHON_VERSION_INFO[(major, minor)] = info 

108 

109 

110LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) 

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

112_FAKE_PAST_VERSION = VersionInfo( 

113 version="0.0", 

114 python_beta=_FAKE_PAST_DATE, 

115 python_start=_FAKE_PAST_DATE, 

116 python_eol=_FAKE_PAST_DATE, 

117) 

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

119_FAKE_FUTURE_VERSION = VersionInfo( 

120 version="999.0", 

121 python_beta=_FAKE_FUTURE_DATE, 

122 python_start=_FAKE_FUTURE_DATE, 

123 python_eol=_FAKE_FUTURE_DATE, 

124) 

125DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=90) 

126EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) 

127 

128 

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

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

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

132 

133 

134def _get_pypi_package_name(module_name): 

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

136 # NOTE(b/578467297, b/578099898): This function is intentionally a no-op 

137 # that returns None. Calling `importlib.metadata.packages_distributions()` 

138 # scans every `.dist-info` directory in `site-packages`, causing severe CPU 

139 # and import-latency overhead across short-lived subprocesses (such as 

140 # Cloud Composer / Airflow DAG parsing and task startups). Furthermore, 

141 # `packages_distributions()` keys by top-level package ("google") rather 

142 # than dotted namespace modules ("google.cloud.foo_v1"), so the lookup 

143 # always missed for Google Cloud packages anyway. 

144 return None 

145 

146 

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

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

149 distribution_package = _get_pypi_package_name(import_package) 

150 dependency_distribution_and_import_packages = ( 

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

152 if distribution_package 

153 else import_package 

154 ) 

155 return dependency_distribution_and_import_packages, distribution_package 

156 

157 

158def check_python_version( 

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

160) -> PythonVersionStatus: 

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

162 

163 Args: 

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

165 

166 Returns: 

167 The support status of the current Python version. 

168 """ 

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

170 

171 python_version = sys.version_info 

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

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

174 

175 version_info = PYTHON_VERSION_INFO.get(version_tuple) 

176 

177 if not version_info: 

178 if version_tuple < LOWEST_TRACKED_VERSION: 

179 version_info = _FAKE_PAST_VERSION 

180 else: 

181 version_info = _FAKE_FUTURE_VERSION 

182 

183 gapic_deprecation = version_info.gapic_deprecation or ( 

184 version_info.python_eol - DEPRECATION_WARNING_PERIOD 

185 ) 

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

187 

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

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

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

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

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

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

194 

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

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

197 def get_package_label(): 

198 label, _ = _get_distribution_and_import_packages(package) 

199 return label 

200 

201 if gapic_end < today: 

202 package_label = get_package_label() 

203 message = _flatten_message( 

204 f""" 

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

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

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

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

209 {package_label}. 

210 """ 

211 ) 

212 warnings.warn(message, FutureWarning) 

213 return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED 

214 

215 eol_date = version_info.python_eol + EOL_GRACE_PERIOD 

216 if eol_date <= today <= gapic_end: 

217 package_label = get_package_label() 

218 message = _flatten_message( 

219 f""" 

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

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

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

223 with any other fixes or features. Please upgrade 

224 to the latest Python version, or at least Python 

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

226 """ 

227 ) 

228 warnings.warn(message, FutureWarning) 

229 return PythonVersionStatus.PYTHON_VERSION_EOL 

230 

231 if gapic_deprecation <= today <= gapic_end: 

232 package_label = get_package_label() 

233 message = _flatten_message( 

234 f""" 

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

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

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

238 latest Python version, or at least Python 

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

240 for {package_label} past that date. 

241 """ 

242 ) 

243 warnings.warn(message, FutureWarning) 

244 return PythonVersionStatus.PYTHON_VERSION_DEPRECATED 

245 

246 return PythonVersionStatus.PYTHON_VERSION_SUPPORTED