Skip to content

Commit c868c2a

Browse files
committed
feat: provide Python run-time version support
This checks the runtime versions. Note that the defaults mean - we get the correct behavior for 3.7 and 3.8: both of these became immediately unsupported; - we get the most conservative behavior for 3.9: it becomes immediately deprecated (desired, since Python 3.9 reaches its end of life in 2025-10), and will become unsupported once it reaches its end of life (which is a conservative policy that we may or may not want to relax in a follow-up) Still todo: echo the package name in the warning message.
1 parent 2c98385 commit c868c2a

File tree

3 files changed

+394
-0
lines changed

3 files changed

+394
-0
lines changed

google/api_core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
This package contains common code and utilities used by Google client libraries.
1818
"""
1919

20+
from google.api_core import _python_version_support
2021
from google.api_core import version as api_core_version
2122

2223
__version__ = api_core_version.__version__
24+
check_python_version = _python_version_support.check_python_version
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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+
17+
import datetime
18+
import enum
19+
import logging
20+
import sys
21+
import textwrap
22+
from typing import NamedTuple, Optional, Dict, Tuple
23+
24+
25+
class PythonVersionStatus(enum.Enum):
26+
"""Represent the support status of a Python version."""
27+
28+
PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED"
29+
PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL"
30+
PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED"
31+
PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED"
32+
33+
34+
class VersionInfo(NamedTuple):
35+
"""Hold release and support date information for a Python version."""
36+
37+
python_beta: Optional[datetime.date]
38+
python_start: datetime.date
39+
python_eol: datetime.date
40+
gapic_start: Optional[datetime.date] = None
41+
gapic_deprecation: Optional[datetime.date] = None
42+
gapic_end: Optional[datetime.date] = None
43+
dep_unpatchable_cve: Optional[datetime.date] = None
44+
45+
46+
PYTHON_VERSION_INFO: Dict[Tuple[int, int], VersionInfo] = {
47+
# Refer to https://devguide.python.org/versions/ and the PEPs linked therefrom.
48+
(3, 7): VersionInfo(
49+
python_beta=None,
50+
python_start=datetime.date(2018, 6, 27),
51+
python_eol=datetime.date(2023, 6, 27),
52+
),
53+
(3, 8): VersionInfo(
54+
python_beta=None,
55+
python_start=datetime.date(2019, 10, 14),
56+
python_eol=datetime.date(2024, 10, 7),
57+
),
58+
(3, 9): VersionInfo(
59+
python_beta=datetime.date(2020, 5, 18),
60+
python_start=datetime.date(2020, 10, 5),
61+
python_eol=datetime.date(2025, 10, 5), # TODO: specify day when announced
62+
),
63+
(3, 10): VersionInfo(
64+
python_beta=datetime.date(2021, 5, 3),
65+
python_start=datetime.date(2021, 10, 4),
66+
python_eol=datetime.date(2026, 10, 4), # TODO: specify day when announced
67+
),
68+
(3, 11): VersionInfo(
69+
python_beta=datetime.date(2022, 5, 8),
70+
python_start=datetime.date(2022, 10, 24),
71+
python_eol=datetime.date(2027, 10, 24), # TODO: specify day when announced
72+
),
73+
(3, 12): VersionInfo(
74+
python_beta=datetime.date(2023, 5, 22),
75+
python_start=datetime.date(2023, 10, 2),
76+
python_eol=datetime.date(2028, 10, 2), # TODO: specify day when announced
77+
),
78+
(3, 13): VersionInfo(
79+
python_beta=datetime.date(2024, 5, 8),
80+
python_start=datetime.date(2024, 10, 7),
81+
python_eol=datetime.date(2029, 10, 7), # TODO: specify day when announced
82+
),
83+
(3, 14): VersionInfo(
84+
python_beta=datetime.date(2025, 5, 7),
85+
python_start=datetime.date(2025, 10, 7),
86+
python_eol=datetime.date(2030, 10, 7), # TODO: specify day when announced
87+
),
88+
}
89+
90+
LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys())
91+
FAKE_PAST_DATE = datetime.date(1970, 1, 1)
92+
FAKE_FUTURE_DATE = datetime.date(9000, 1, 1)
93+
94+
95+
def _flatten_message(text: str) -> str:
96+
"""Dedent a multi-line string and flattens it into a single line."""
97+
return textwrap.dedent(text).strip().replace("\n", " ")
98+
99+
100+
def check_python_version(today: Optional[datetime.date] = None) -> PythonVersionStatus:
101+
"""Check the running Python version and issue a support warning if needed.
102+
103+
Args:
104+
today: The date to check against. Defaults to the current date.
105+
106+
Returns:
107+
The support status of the current Python version.
108+
"""
109+
today = today or datetime.date.today()
110+
111+
python_version = sys.version_info
112+
version_tuple = (python_version.major, python_version.minor)
113+
py_version_str = f"{python_version.major}.{python_version.minor}"
114+
115+
version_info = PYTHON_VERSION_INFO.get(version_tuple)
116+
117+
if not version_info:
118+
if version_tuple < LOWEST_TRACKED_VERSION:
119+
version_info = VersionInfo(
120+
python_beta=FAKE_PAST_DATE,
121+
python_start=FAKE_PAST_DATE,
122+
python_eol=FAKE_PAST_DATE,
123+
)
124+
else:
125+
version_info = VersionInfo(
126+
python_beta=FAKE_FUTURE_DATE,
127+
python_start=FAKE_FUTURE_DATE,
128+
python_eol=FAKE_FUTURE_DATE,
129+
)
130+
131+
gapic_deprecation = version_info.gapic_deprecation or (
132+
version_info.python_eol - datetime.timedelta(days=365)
133+
)
134+
gapic_end = version_info.gapic_end or (
135+
version_info.python_eol + datetime.timedelta(weeks=1)
136+
)
137+
138+
def min_python(date: datetime.date) -> str:
139+
"""Find the minimum supported Python version for a given date."""
140+
for version, info in sorted(PYTHON_VERSION_INFO.items()):
141+
if info.python_start <= date < info.python_eol:
142+
return f"{version[0]}.{version[1]}"
143+
return "at a supported version"
144+
145+
if gapic_end < today:
146+
message = _flatten_message(
147+
f"""
148+
You are using a non-supported Python version ({py_version_str}).
149+
You will receive no updates to this client library. We suggest
150+
you upgrade to the latest Python version, or at least Python
151+
{min_python(today)}, and then update this library.
152+
"""
153+
)
154+
logging.warning(message)
155+
return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED
156+
157+
eol_date = version_info.python_eol + datetime.timedelta(weeks=1)
158+
if eol_date <= today <= gapic_end:
159+
message = _flatten_message(
160+
f"""
161+
You are using a Python version ({py_version_str}) past its end
162+
of life. This client library will continue receiving critical
163+
bug fixes on a best-effort basis, but not any other fixes or
164+
features. We suggest you upgrade to the latest Python version,
165+
or at least Python {min_python(today)}, and then update this
166+
library.
167+
"""
168+
)
169+
logging.warning(message)
170+
return PythonVersionStatus.PYTHON_VERSION_EOL
171+
172+
if gapic_deprecation <= today <= gapic_end:
173+
message = _flatten_message(
174+
f"""
175+
You are using a Python version ({py_version_str}), which new
176+
releases of this client library will stop supporting when it
177+
reaches its end of life ({version_info.python_eol}). We
178+
suggest you upgrade to the latest Python version, or at least
179+
Python {min_python(version_info.python_eol)}, and then update
180+
this library.
181+
"""
182+
)
183+
logging.warning(message)
184+
return PythonVersionStatus.PYTHON_VERSION_DEPRECATED
185+
186+
return PythonVersionStatus.PYTHON_VERSION_SUPPORTED

0 commit comments

Comments
 (0)