Skip to content

Commit a5f9742

Browse files
authored
drop end of life python versions (#5731)
2 parents e7e5380 + 52df9ee commit a5f9742

File tree

11 files changed

+78
-284
lines changed

11 files changed

+78
-284
lines changed

.github/workflows/tests.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ jobs:
1919
- {python: '3.12'}
2020
- {python: '3.11'}
2121
- {python: '3.10'}
22-
- {python: '3.9'}
2322
- {name: PyPy, python: 'pypy-3.11', tox: pypy3.11}
2423
- {name: Minimum Versions, python: '3.13', tox: tests-min}
2524
- {name: Development Versions, python: '3.10', tox: tests-dev}

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Version 3.2.0
33

44
Unreleased
55

6+
- Drop support for Python 3.9. :pr:`5730`
67
- Remove previously deprecated code: ``__version__``. :pr:`5648`
78

89

docs/extensiondev.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,13 @@ ecosystem remain consistent and compatible.
294294
indicate minimum compatibility support. For example,
295295
``sqlalchemy>=1.4``.
296296
9. Indicate the versions of Python supported using ``python_requires=">=version"``.
297-
Flask itself supports Python >=3.9 as of October 2024, and this will update
298-
over time.
297+
Flask and Pallets policy is to support all Python versions that are not
298+
within six months of end of life (EOL). See Python's `EOL calendar`_ for
299+
timing.
299300

300301
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask
301302
.. _Discord Chat: https://discord.gg/pallets
302303
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
303304
.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/
304305
.. _Pallets-Eco: https://github.com/pallets-eco
306+
.. _EOL calendar: https://devguide.python.org/versions/

docs/installation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Installation
55
Python Version
66
--------------
77

8-
We recommend using the latest version of Python. Flask supports Python 3.9 and newer.
8+
We recommend using the latest version of Python. Flask supports Python 3.10 and newer.
99

1010

1111
Dependencies

pyproject.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ classifiers = [
1919
"Topic :: Software Development :: Libraries :: Application Frameworks",
2020
"Typing :: Typed",
2121
]
22-
requires-python = ">=3.9"
22+
requires-python = ">=3.10"
2323
dependencies = [
2424
"blinker>=1.9.0",
2525
"click>=8.1.3",
26-
"importlib-metadata>=3.6.0; python_version < '3.10'",
2726
"itsdangerous>=2.2.0",
2827
"jinja2>=3.1.2",
2928
"markupsafe>=2.1.1",
@@ -58,7 +57,7 @@ pre-commit = [
5857
]
5958
tests = [
6059
"asgiref",
61-
"greenlet ; python_version < '3.11'",
60+
"greenlet",
6261
"pytest",
6362
"python-dotenv",
6463
]
@@ -126,7 +125,7 @@ exclude_also = [
126125
]
127126

128127
[tool.mypy]
129-
python_version = "3.9"
128+
python_version = "3.10"
130129
files = ["src", "tests/type_check"]
131130
show_error_codes = true
132131
pretty = true
@@ -142,7 +141,7 @@ module = [
142141
ignore_missing_imports = true
143142

144143
[tool.pyright]
145-
pythonVersion = "3.9"
144+
pythonVersion = "3.10"
146145
include = ["src", "tests/type_check"]
147146
typeCheckingMode = "basic"
148147

@@ -161,6 +160,9 @@ select = [
161160
"UP", # pyupgrade
162161
"W", # pycodestyle warning
163162
]
163+
ignore = [
164+
"UP038", # keep isinstance tuple
165+
]
164166

165167
[tool.ruff.lint.isort]
166168
force-single-line = true
@@ -173,7 +175,7 @@ tag-only = [
173175

174176
[tool.tox]
175177
env_list = [
176-
"py3.13", "py3.12", "py3.11", "py3.10", "py3.9",
178+
"py3.13", "py3.12", "py3.11", "py3.10",
177179
"pypy3.11",
178180
"tests-min", "tests-dev",
179181
"style",

src/flask/cli.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -601,15 +601,7 @@ def _load_plugin_commands(self) -> None:
601601
if self._loaded_plugin_commands:
602602
return
603603

604-
if sys.version_info >= (3, 10):
605-
from importlib import metadata
606-
else:
607-
# Use a backport on Python < 3.10. We technically have
608-
# importlib.metadata on 3.8+, but the API changed in 3.10,
609-
# so use the backport for consistency.
610-
import importlib_metadata as metadata # pyright: ignore
611-
612-
for ep in metadata.entry_points(group="flask.commands"):
604+
for ep in importlib.metadata.entry_points(group="flask.commands"):
613605
self.add_command(ep.load(), ep.name)
614606

615607
self._loaded_plugin_commands = True

src/flask/json/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class DefaultJSONProvider(JSONProvider):
135135
method) will call the ``__html__`` method to get a string.
136136
"""
137137

138-
default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment]
138+
default: t.Callable[[t.Any], t.Any] = staticmethod(_default)
139139
"""Apply this function to any object that :meth:`json.dumps` does
140140
not know how to serialize. It should return a valid JSON type or
141141
raise a ``TypeError``.

src/flask/sansio/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ class App(Scaffold):
213213
#:
214214
#: This attribute can also be configured from the config with the
215215
#: :data:`SECRET_KEY` configuration key. Defaults to ``None``.
216-
secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY")
216+
secret_key = ConfigAttribute[str | bytes | None]("SECRET_KEY")
217217

218218
#: A :class:`~datetime.timedelta` which is used to set the expiration
219219
#: date of a permanent session. The default is 31 days which makes a

src/flask/typing.py

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
]
2424

2525
# the possible types for an individual HTTP header
26-
# This should be a Union, but mypy doesn't pass unless it's a TypeVar.
27-
HeaderValue = t.Union[str, list[str], tuple[str, ...]]
26+
HeaderValue = str | list[str] | tuple[str, ...]
2827

2928
# the possible types for HTTP headers
3029
HeadersValue = t.Union[
@@ -47,47 +46,42 @@
4746
# callback annotated with flask.Response fail type checking.
4847
ResponseClass = t.TypeVar("ResponseClass", bound="Response")
4948

50-
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
51-
AfterRequestCallable = t.Union[
52-
t.Callable[[ResponseClass], ResponseClass],
53-
t.Callable[[ResponseClass], t.Awaitable[ResponseClass]],
54-
]
55-
BeforeFirstRequestCallable = t.Union[
56-
t.Callable[[], None], t.Callable[[], t.Awaitable[None]]
57-
]
58-
BeforeRequestCallable = t.Union[
59-
t.Callable[[], t.Optional[ResponseReturnValue]],
60-
t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]],
61-
]
49+
AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named
50+
AfterRequestCallable = (
51+
t.Callable[[ResponseClass], ResponseClass]
52+
| t.Callable[[ResponseClass], t.Awaitable[ResponseClass]]
53+
)
54+
BeforeFirstRequestCallable = t.Callable[[], None] | t.Callable[[], t.Awaitable[None]]
55+
BeforeRequestCallable = (
56+
t.Callable[[], ResponseReturnValue | None]
57+
| t.Callable[[], t.Awaitable[ResponseReturnValue | None]]
58+
)
6259
ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]]
63-
TeardownCallable = t.Union[
64-
t.Callable[[t.Optional[BaseException]], None],
65-
t.Callable[[t.Optional[BaseException]], t.Awaitable[None]],
66-
]
67-
TemplateContextProcessorCallable = t.Union[
68-
t.Callable[[], dict[str, t.Any]],
69-
t.Callable[[], t.Awaitable[dict[str, t.Any]]],
70-
]
60+
TeardownCallable = (
61+
t.Callable[[BaseException | None], None]
62+
| t.Callable[[BaseException | None], t.Awaitable[None]]
63+
)
64+
TemplateContextProcessorCallable = (
65+
t.Callable[[], dict[str, t.Any]] | t.Callable[[], t.Awaitable[dict[str, t.Any]]]
66+
)
7167
TemplateFilterCallable = t.Callable[..., t.Any]
7268
TemplateGlobalCallable = t.Callable[..., t.Any]
7369
TemplateTestCallable = t.Callable[..., bool]
7470
URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None]
75-
URLValuePreprocessorCallable = t.Callable[
76-
[t.Optional[str], t.Optional[dict[str, t.Any]]], None
77-
]
71+
URLValuePreprocessorCallable = t.Callable[[str | None, dict[str, t.Any] | None], None]
7872

7973
# This should take Exception, but that either breaks typing the argument
8074
# with a specific exception, or decorating multiple times with different
8175
# exceptions (and using a union type on the argument).
8276
# https://github.com/pallets/flask/issues/4095
8377
# https://github.com/pallets/flask/issues/4295
8478
# https://github.com/pallets/flask/issues/4297
85-
ErrorHandlerCallable = t.Union[
86-
t.Callable[[t.Any], ResponseReturnValue],
87-
t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]],
88-
]
79+
ErrorHandlerCallable = (
80+
t.Callable[[t.Any], ResponseReturnValue]
81+
| t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]]
82+
)
8983

90-
RouteCallable = t.Union[
91-
t.Callable[..., ResponseReturnValue],
92-
t.Callable[..., t.Awaitable[ResponseReturnValue]],
93-
]
84+
RouteCallable = (
85+
t.Callable[..., ResponseReturnValue]
86+
| t.Callable[..., t.Awaitable[ResponseReturnValue]]
87+
)

tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def invoke(self, app, runner):
462462

463463
def expect_order(self, order, output):
464464
# skip the header and match the start of each row
465-
for expect, line in zip(order, output.splitlines()[2:]):
465+
for expect, line in zip(order, output.splitlines()[2:], strict=False):
466466
# do this instead of startswith for nicer pytest output
467467
assert line[: len(expect)] == expect
468468

0 commit comments

Comments
 (0)