Skip to content

Commit 5d12b86

Browse files
authored
Drop support for Python 3.7 (#2178)
1 parent 12d9659 commit 5d12b86

File tree

14 files changed

+44
-110
lines changed

14 files changed

+44
-110
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- uses: "actions/checkout@v3"
1818
- uses: "actions/setup-python@v4"
1919
with:
20-
python-version: 3.7
20+
python-version: 3.10
2121
- name: "Install dependencies"
2222
run: "scripts/install"
2323
- name: "Build package & docs"

.github/workflows/test-suite.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
17+
python-version: ["3.8", "3.9", "3.10", "3.11"]
1818

1919
steps:
2020
- uses: "actions/checkout@v3"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ It is production-ready, and gives you the following:
4141

4242
## Requirements
4343

44-
Python 3.7+ (For Python 3.6 support, install version 0.19.1)
44+
Python 3.8+
4545

4646
## Installation
4747

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ It is production-ready, and gives you the following:
3838

3939
## Requirements
4040

41-
Python 3.7+ (For Python 3.6 support, install version 0.19.1)
41+
Python 3.8+
4242

4343
## Installation
4444

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dynamic = ["version"]
88
description = "The little ASGI library that shines."
99
readme = "README.md"
1010
license = "BSD-3-Clause"
11-
requires-python = ">=3.7"
11+
requires-python = ">=3.8"
1212
authors = [
1313
{ name = "Tom Christie", email = "[email protected]" },
1414
]
@@ -20,7 +20,6 @@ classifiers = [
2020
"License :: OSI Approved :: BSD License",
2121
"Operating System :: OS Independent",
2222
"Programming Language :: Python :: 3",
23-
"Programming Language :: Python :: 3.7",
2423
"Programming Language :: Python :: 3.8",
2524
"Programming Language :: Python :: 3.9",
2625
"Programming Language :: Python :: 3.10",

starlette/_utils.py

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import asyncio
22
import functools
3-
import sys
43
import typing
5-
from types import TracebackType
6-
7-
if sys.version_info < (3, 8): # pragma: no cover
8-
from typing_extensions import Protocol
9-
else: # pragma: no cover
10-
from typing import Protocol
114

125

136
def is_async_callable(obj: typing.Any) -> bool:
@@ -22,31 +15,13 @@ def is_async_callable(obj: typing.Any) -> bool:
2215
T_co = typing.TypeVar("T_co", covariant=True)
2316

2417

25-
# TODO: once 3.8 is the minimum supported version (27 Jun 2023)
26-
# this can just become
27-
# class AwaitableOrContextManager(
28-
# typing.Awaitable[T_co],
29-
# typing.AsyncContextManager[T_co],
30-
# typing.Protocol[T_co],
31-
# ):
32-
# pass
33-
class AwaitableOrContextManager(Protocol[T_co]):
34-
def __await__(self) -> typing.Generator[typing.Any, None, T_co]:
35-
... # pragma: no cover
36-
37-
async def __aenter__(self) -> T_co:
38-
... # pragma: no cover
39-
40-
async def __aexit__(
41-
self,
42-
__exc_type: typing.Optional[typing.Type[BaseException]],
43-
__exc_value: typing.Optional[BaseException],
44-
__traceback: typing.Optional[TracebackType],
45-
) -> typing.Union[bool, None]:
46-
... # pragma: no cover
18+
class AwaitableOrContextManager(
19+
typing.Awaitable[T_co], typing.AsyncContextManager[T_co], typing.Protocol[T_co]
20+
):
21+
...
4722

4823

49-
class SupportsAsyncClose(Protocol):
24+
class SupportsAsyncClose(typing.Protocol):
5025
async def close(self) -> None:
5126
... # pragma: no cover
5227

starlette/applications.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,8 @@ def build_middleware_stack(self) -> ASGIApp:
111111
def routes(self) -> typing.List[BaseRoute]:
112112
return self.router.routes
113113

114-
# TODO: Make `__name` a positional-only argument when we drop Python 3.7 support.
115-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
116-
return self.router.url_path_for(__name, **path_params)
114+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
115+
return self.router.url_path_for(name, **path_params)
117116

118117
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
119118
scope["app"] = self

starlette/middleware/sessions.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
import sys
32
import typing
43
from base64 import b64decode, b64encode
54

@@ -10,11 +9,6 @@
109
from starlette.requests import HTTPConnection
1110
from starlette.types import ASGIApp, Message, Receive, Scope, Send
1211

13-
if sys.version_info >= (3, 8): # pragma: no cover
14-
from typing import Literal
15-
else: # pragma: no cover
16-
from typing_extensions import Literal
17-
1812

1913
class SessionMiddleware:
2014
def __init__(
@@ -24,7 +18,7 @@ def __init__(
2418
session_cookie: str = "session",
2519
max_age: typing.Optional[int] = 14 * 24 * 60 * 60, # 14 days, in seconds
2620
path: str = "/",
27-
same_site: Literal["lax", "strict", "none"] = "lax",
21+
same_site: typing.Literal["lax", "strict", "none"] = "lax",
2822
https_only: bool = False,
2923
) -> None:
3024
self.app = app

starlette/requests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ def state(self) -> State:
173173
self._state = State(self.scope["state"])
174174
return self._state
175175

176-
def url_for(self, __name: str, **path_params: typing.Any) -> URL:
176+
def url_for(self, name: str, /, **path_params: typing.Any) -> URL:
177177
router: Router = self.scope["router"]
178-
url_path = router.url_path_for(__name, **path_params)
178+
url_path = router.url_path_for(name, **path_params)
179179
return url_path.make_absolute_url(base_url=self.base_url)
180180

181181

starlette/responses.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
import json
33
import os
44
import stat
5-
import sys
65
import typing
76
from datetime import datetime
87
from email.utils import format_datetime, formatdate
98
from functools import partial
10-
from mimetypes import guess_type as mimetypes_guess_type
9+
from mimetypes import guess_type
1110
from urllib.parse import quote
1211

1312
import anyio
@@ -18,23 +17,6 @@
1817
from starlette.datastructures import URL, MutableHeaders
1918
from starlette.types import Receive, Scope, Send
2019

21-
if sys.version_info >= (3, 8): # pragma: no cover
22-
from typing import Literal
23-
else: # pragma: no cover
24-
from typing_extensions import Literal
25-
26-
# Workaround for adding samesite support to pre 3.8 python
27-
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore[attr-defined]
28-
29-
30-
# Compatibility wrapper for `mimetypes.guess_type` to support `os.PathLike` on <py3.8
31-
def guess_type(
32-
url: typing.Union[str, "os.PathLike[str]"], strict: bool = True
33-
) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
34-
if sys.version_info < (3, 8): # pragma: no cover
35-
url = os.fspath(url)
36-
return mimetypes_guess_type(url, strict)
37-
3820

3921
class Response:
4022
media_type = None
@@ -111,7 +93,7 @@ def set_cookie(
11193
domain: typing.Optional[str] = None,
11294
secure: bool = False,
11395
httponly: bool = False,
114-
samesite: typing.Optional[Literal["lax", "strict", "none"]] = "lax",
96+
samesite: typing.Optional[typing.Literal["lax", "strict", "none"]] = "lax",
11597
) -> None:
11698
cookie: "http.cookies.BaseCookie[str]" = http.cookies.SimpleCookie()
11799
cookie[key] = value
@@ -147,7 +129,7 @@ def delete_cookie(
147129
domain: typing.Optional[str] = None,
148130
secure: bool = False,
149131
httponly: bool = False,
150-
samesite: typing.Optional[Literal["lax", "strict", "none"]] = "lax",
132+
samesite: typing.Optional[typing.Literal["lax", "strict", "none"]] = "lax",
151133
) -> None:
152134
self.set_cookie(
153135
key,

starlette/routing.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class BaseRoute:
179179
def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
180180
raise NotImplementedError() # pragma: no cover
181181

182-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
182+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
183183
raise NotImplementedError() # pragma: no cover
184184

185185
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
@@ -258,12 +258,12 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
258258
return Match.FULL, child_scope
259259
return Match.NONE, {}
260260

261-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
261+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
262262
seen_params = set(path_params.keys())
263263
expected_params = set(self.param_convertors.keys())
264264

265-
if __name != self.name or seen_params != expected_params:
266-
raise NoMatchFound(__name, path_params)
265+
if name != self.name or seen_params != expected_params:
266+
raise NoMatchFound(name, path_params)
267267

268268
path, remaining_params = replace_params(
269269
self.path_format, self.param_convertors, path_params
@@ -333,12 +333,12 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
333333
return Match.FULL, child_scope
334334
return Match.NONE, {}
335335

336-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
336+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
337337
seen_params = set(path_params.keys())
338338
expected_params = set(self.param_convertors.keys())
339339

340-
if __name != self.name or seen_params != expected_params:
341-
raise NoMatchFound(__name, path_params)
340+
if name != self.name or seen_params != expected_params:
341+
raise NoMatchFound(name, path_params)
342342

343343
path, remaining_params = replace_params(
344344
self.path_format, self.param_convertors, path_params
@@ -415,22 +415,22 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
415415
return Match.FULL, child_scope
416416
return Match.NONE, {}
417417

418-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
419-
if self.name is not None and __name == self.name and "path" in path_params:
418+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
419+
if self.name is not None and name == self.name and "path" in path_params:
420420
# 'name' matches "<mount_name>".
421421
path_params["path"] = path_params["path"].lstrip("/")
422422
path, remaining_params = replace_params(
423423
self.path_format, self.param_convertors, path_params
424424
)
425425
if not remaining_params:
426426
return URLPath(path=path)
427-
elif self.name is None or __name.startswith(self.name + ":"):
427+
elif self.name is None or name.startswith(self.name + ":"):
428428
if self.name is None:
429429
# No mount name.
430-
remaining_name = __name
430+
remaining_name = name
431431
else:
432432
# 'name' matches "<mount_name>:<child_name>".
433-
remaining_name = __name[len(self.name) + 1 :]
433+
remaining_name = name[len(self.name) + 1 :]
434434
path_kwarg = path_params.get("path")
435435
path_params["path"] = ""
436436
path_prefix, remaining_params = replace_params(
@@ -446,7 +446,7 @@ def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
446446
)
447447
except NoMatchFound:
448448
pass
449-
raise NoMatchFound(__name, path_params)
449+
raise NoMatchFound(name, path_params)
450450

451451
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
452452
await self.app(scope, receive, send)
@@ -493,22 +493,22 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
493493
return Match.FULL, child_scope
494494
return Match.NONE, {}
495495

496-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
497-
if self.name is not None and __name == self.name and "path" in path_params:
496+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
497+
if self.name is not None and name == self.name and "path" in path_params:
498498
# 'name' matches "<mount_name>".
499499
path = path_params.pop("path")
500500
host, remaining_params = replace_params(
501501
self.host_format, self.param_convertors, path_params
502502
)
503503
if not remaining_params:
504504
return URLPath(path=path, host=host)
505-
elif self.name is None or __name.startswith(self.name + ":"):
505+
elif self.name is None or name.startswith(self.name + ":"):
506506
if self.name is None:
507507
# No mount name.
508-
remaining_name = __name
508+
remaining_name = name
509509
else:
510510
# 'name' matches "<mount_name>:<child_name>".
511-
remaining_name = __name[len(self.name) + 1 :]
511+
remaining_name = name[len(self.name) + 1 :]
512512
host, remaining_params = replace_params(
513513
self.host_format, self.param_convertors, path_params
514514
)
@@ -518,7 +518,7 @@ def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
518518
return URLPath(path=str(url), protocol=url.protocol, host=host)
519519
except NoMatchFound:
520520
pass
521-
raise NoMatchFound(__name, path_params)
521+
raise NoMatchFound(name, path_params)
522522

523523
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
524524
await self.app(scope, receive, send)
@@ -652,13 +652,13 @@ async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None:
652652
response = PlainTextResponse("Not Found", status_code=404)
653653
await response(scope, receive, send)
654654

655-
def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
655+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
656656
for route in self.routes:
657657
try:
658-
return route.url_path_for(__name, **path_params)
658+
return route.url_path_for(name, **path_params)
659659
except NoMatchFound:
660660
pass
661-
raise NoMatchFound(__name, path_params)
661+
raise NoMatchFound(name, path_params)
662662

663663
async def startup(self) -> None:
664664
"""

starlette/templating.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,9 @@ def _create_env(
123123
**env_options: typing.Any,
124124
) -> "jinja2.Environment":
125125
@pass_context
126-
# TODO: Make `__name` a positional-only argument when we drop Python 3.7
127-
# support.
128-
def url_for(context: dict, __name: str, **path_params: typing.Any) -> URL:
126+
def url_for(context: dict, name: str, /, **path_params: typing.Any) -> URL:
129127
request = context["request"]
130-
return request.url_for(__name, **path_params)
128+
return request.url_for(name, **path_params)
131129

132130
loader = jinja2.FileSystemLoader(directory)
133131
env_options.setdefault("loader", loader)

starlette/testclient.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import json
55
import math
66
import queue
7-
import sys
87
import typing
98
import warnings
109
from concurrent.futures import Future
@@ -27,12 +26,6 @@
2726
"You can install this with:\n"
2827
" $ pip install httpx\n"
2928
)
30-
31-
if sys.version_info >= (3, 8): # pragma: no cover
32-
from typing import TypedDict
33-
else: # pragma: no cover
34-
from typing_extensions import TypedDict
35-
3629
_PortalFactoryType = typing.Callable[
3730
[], typing.ContextManager[anyio.abc.BlockingPortal]
3831
]
@@ -64,7 +57,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
6457
await instance(receive, send)
6558

6659

67-
class _AsyncBackend(TypedDict):
60+
class _AsyncBackend(typing.TypedDict):
6861
backend: str
6962
backend_options: typing.Dict[str, typing.Any]
7063

0 commit comments

Comments
 (0)