]> git.ipfire.org Git - thirdparty/starlette.git/commitdiff
Drop support for Python 3.7 (#2178)
authorMarcelo Trylesinski <marcelotryle@gmail.com>
Thu, 13 Jul 2023 08:19:12 +0000 (10:19 +0200)
committerGitHub <noreply@github.com>
Thu, 13 Jul 2023 08:19:12 +0000 (02:19 -0600)
14 files changed:
.github/workflows/publish.yml
.github/workflows/test-suite.yml
README.md
docs/index.md
pyproject.toml
starlette/_utils.py
starlette/applications.py
starlette/middleware/sessions.py
starlette/requests.py
starlette/responses.py
starlette/routing.py
starlette/templating.py
starlette/testclient.py
tests/test_routing.py

index 85350472ccdabc9fde251737f805af24db253436..d714d038f164f9110f77812d73c7b760b92e81f1 100644 (file)
@@ -17,7 +17,7 @@ jobs:
       - uses: "actions/checkout@v3"
       - uses: "actions/setup-python@v4"
         with:
-          python-version: 3.7
+          python-version: 3.10
       - name: "Install dependencies"
         run: "scripts/install"
       - name: "Build package & docs"
index f5ad75a4e29c08609e478814cbbbf1020284a067..eb1cc7e22e16dea33d875079c5166102ef5542ae 100644 (file)
@@ -14,7 +14,7 @@ jobs:
 
     strategy:
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
 
     steps:
       - uses: "actions/checkout@v3"
index 6aff26b3e517892327c1707626a4cadd044f46bd..d03eec16e6af8c3cb9a5098670135378f425ed44 100644 (file)
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ It is production-ready, and gives you the following:
 
 ## Requirements
 
-Python 3.7+ (For Python 3.6 support, install version 0.19.1)
+Python 3.8+
 
 ## Installation
 
index 20435d2620ebb9fe4410a72d5dde3367436368a9..e395800f560e07eb97bcc93b8533b1472251587f 100644 (file)
@@ -38,7 +38,7 @@ It is production-ready, and gives you the following:
 
 ## Requirements
 
-Python 3.7+ (For Python 3.6 support, install version 0.19.1)
+Python 3.8+
 
 ## Installation
 
index 3ff3ca71bfc028901326422af893faacd899db1e..0b8e6cb50e7726065faa4b6c86ab63df0a67a86f 100644 (file)
@@ -8,7 +8,7 @@ dynamic = ["version"]
 description = "The little ASGI library that shines."
 readme = "README.md"
 license = "BSD-3-Clause"
-requires-python = ">=3.7"
+requires-python = ">=3.8"
 authors = [
     { name = "Tom Christie", email = "tom@tomchristie.com" },
 ]
@@ -20,7 +20,6 @@ classifiers = [
     "License :: OSI Approved :: BSD License",
     "Operating System :: OS Independent",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
index d781647ff74f05d536a9ff675b2d7114738c4681..5a6e6965b9a628e471c3c8aa1f0d3bf30cf2f4e6 100644 (file)
@@ -1,13 +1,6 @@
 import asyncio
 import functools
-import sys
 import typing
-from types import TracebackType
-
-if sys.version_info < (3, 8):  # pragma: no cover
-    from typing_extensions import Protocol
-else:  # pragma: no cover
-    from typing import Protocol
 
 
 def is_async_callable(obj: typing.Any) -> bool:
@@ -22,31 +15,13 @@ def is_async_callable(obj: typing.Any) -> bool:
 T_co = typing.TypeVar("T_co", covariant=True)
 
 
-# TODO: once 3.8 is the minimum supported version (27 Jun 2023)
-# this can just become
-# class AwaitableOrContextManager(
-#     typing.Awaitable[T_co],
-#     typing.AsyncContextManager[T_co],
-#     typing.Protocol[T_co],
-# ):
-#     pass
-class AwaitableOrContextManager(Protocol[T_co]):
-    def __await__(self) -> typing.Generator[typing.Any, None, T_co]:
-        ...  # pragma: no cover
-
-    async def __aenter__(self) -> T_co:
-        ...  # pragma: no cover
-
-    async def __aexit__(
-        self,
-        __exc_type: typing.Optional[typing.Type[BaseException]],
-        __exc_value: typing.Optional[BaseException],
-        __traceback: typing.Optional[TracebackType],
-    ) -> typing.Union[bool, None]:
-        ...  # pragma: no cover
+class AwaitableOrContextManager(
+    typing.Awaitable[T_co], typing.AsyncContextManager[T_co], typing.Protocol[T_co]
+):
+    ...
 
 
-class SupportsAsyncClose(Protocol):
+class SupportsAsyncClose(typing.Protocol):
     async def close(self) -> None:
         ...  # pragma: no cover
 
index 5fc11f9555b21da896a33c43dcbd4b4fd2aa5ce0..344a4a37fe66d2684c80b1c7fdceaa19e904318a 100644 (file)
@@ -111,9 +111,8 @@ class Starlette:
     def routes(self) -> typing.List[BaseRoute]:
         return self.router.routes
 
-    # TODO: Make `__name` a positional-only argument when we drop Python 3.7 support.
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
-        return self.router.url_path_for(__name, **path_params)
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+        return self.router.url_path_for(name, **path_params)
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         scope["app"] = self
index b1e32ec16109987d25195f9c76613ebed19dc9ed..74e57352f5f9628914b3da434eb9729086879270 100644 (file)
@@ -1,5 +1,4 @@
 import json
-import sys
 import typing
 from base64 import b64decode, b64encode
 
@@ -10,11 +9,6 @@ from starlette.datastructures import MutableHeaders, Secret
 from starlette.requests import HTTPConnection
 from starlette.types import ASGIApp, Message, Receive, Scope, Send
 
-if sys.version_info >= (3, 8):  # pragma: no cover
-    from typing import Literal
-else:  # pragma: no cover
-    from typing_extensions import Literal
-
 
 class SessionMiddleware:
     def __init__(
@@ -24,7 +18,7 @@ class SessionMiddleware:
         session_cookie: str = "session",
         max_age: typing.Optional[int] = 14 * 24 * 60 * 60,  # 14 days, in seconds
         path: str = "/",
-        same_site: Literal["lax", "strict", "none"] = "lax",
+        same_site: typing.Literal["lax", "strict", "none"] = "lax",
         https_only: bool = False,
     ) -> None:
         self.app = app
index dbcaad87500e7b72a5b72e6be00555c2bc10dcf7..fff451e2321280e29044b8546d0d065caa46bd3b 100644 (file)
@@ -173,9 +173,9 @@ class HTTPConnection(typing.Mapping[str, typing.Any]):
             self._state = State(self.scope["state"])
         return self._state
 
-    def url_for(self, __name: str, **path_params: typing.Any) -> URL:
+    def url_for(self, name: str, /, **path_params: typing.Any) -> URL:
         router: Router = self.scope["router"]
-        url_path = router.url_path_for(__name, **path_params)
+        url_path = router.url_path_for(name, **path_params)
         return url_path.make_absolute_url(base_url=self.base_url)
 
 
index 24fece1d481d98ca875dbcf80d01656d97bd45e0..575caf655b115f1e8780d09bbfedec111557af11 100644 (file)
@@ -2,12 +2,11 @@ import http.cookies
 import json
 import os
 import stat
-import sys
 import typing
 from datetime import datetime
 from email.utils import format_datetime, formatdate
 from functools import partial
-from mimetypes import guess_type as mimetypes_guess_type
+from mimetypes import guess_type
 from urllib.parse import quote
 
 import anyio
@@ -18,23 +17,6 @@ from starlette.concurrency import iterate_in_threadpool
 from starlette.datastructures import URL, MutableHeaders
 from starlette.types import Receive, Scope, Send
 
-if sys.version_info >= (3, 8):  # pragma: no cover
-    from typing import Literal
-else:  # pragma: no cover
-    from typing_extensions import Literal
-
-# Workaround for adding samesite support to pre 3.8 python
-http.cookies.Morsel._reserved["samesite"] = "SameSite"  # type: ignore[attr-defined]
-
-
-# Compatibility wrapper for `mimetypes.guess_type` to support `os.PathLike` on <py3.8
-def guess_type(
-    url: typing.Union[str, "os.PathLike[str]"], strict: bool = True
-) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
-    if sys.version_info < (3, 8):  # pragma: no cover
-        url = os.fspath(url)
-    return mimetypes_guess_type(url, strict)
-
 
 class Response:
     media_type = None
@@ -111,7 +93,7 @@ class Response:
         domain: typing.Optional[str] = None,
         secure: bool = False,
         httponly: bool = False,
-        samesite: typing.Optional[Literal["lax", "strict", "none"]] = "lax",
+        samesite: typing.Optional[typing.Literal["lax", "strict", "none"]] = "lax",
     ) -> None:
         cookie: "http.cookies.BaseCookie[str]" = http.cookies.SimpleCookie()
         cookie[key] = value
@@ -147,7 +129,7 @@ class Response:
         domain: typing.Optional[str] = None,
         secure: bool = False,
         httponly: bool = False,
-        samesite: typing.Optional[Literal["lax", "strict", "none"]] = "lax",
+        samesite: typing.Optional[typing.Literal["lax", "strict", "none"]] = "lax",
     ) -> None:
         self.set_cookie(
             key,
index 132600be60b963f0d17e74effc7e05d9a6492fa1..b50d32a1fc6d6247e7791bef9019a7580b5b06f5 100644 (file)
@@ -179,7 +179,7 @@ class BaseRoute:
     def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
         raise NotImplementedError()  # pragma: no cover
 
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
         raise NotImplementedError()  # pragma: no cover
 
     async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
@@ -258,12 +258,12 @@ class Route(BaseRoute):
                     return Match.FULL, child_scope
         return Match.NONE, {}
 
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
         seen_params = set(path_params.keys())
         expected_params = set(self.param_convertors.keys())
 
-        if __name != self.name or seen_params != expected_params:
-            raise NoMatchFound(__name, path_params)
+        if name != self.name or seen_params != expected_params:
+            raise NoMatchFound(name, path_params)
 
         path, remaining_params = replace_params(
             self.path_format, self.param_convertors, path_params
@@ -333,12 +333,12 @@ class WebSocketRoute(BaseRoute):
                 return Match.FULL, child_scope
         return Match.NONE, {}
 
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
         seen_params = set(path_params.keys())
         expected_params = set(self.param_convertors.keys())
 
-        if __name != self.name or seen_params != expected_params:
-            raise NoMatchFound(__name, path_params)
+        if name != self.name or seen_params != expected_params:
+            raise NoMatchFound(name, path_params)
 
         path, remaining_params = replace_params(
             self.path_format, self.param_convertors, path_params
@@ -415,8 +415,8 @@ class Mount(BaseRoute):
                 return Match.FULL, child_scope
         return Match.NONE, {}
 
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
-        if self.name is not None and __name == self.name and "path" in path_params:
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+        if self.name is not None and name == self.name and "path" in path_params:
             # 'name' matches "<mount_name>".
             path_params["path"] = path_params["path"].lstrip("/")
             path, remaining_params = replace_params(
@@ -424,13 +424,13 @@ class Mount(BaseRoute):
             )
             if not remaining_params:
                 return URLPath(path=path)
-        elif self.name is None or __name.startswith(self.name + ":"):
+        elif self.name is None or name.startswith(self.name + ":"):
             if self.name is None:
                 # No mount name.
-                remaining_name = __name
+                remaining_name = name
             else:
                 # 'name' matches "<mount_name>:<child_name>".
-                remaining_name = __name[len(self.name) + 1 :]
+                remaining_name = name[len(self.name) + 1 :]
             path_kwarg = path_params.get("path")
             path_params["path"] = ""
             path_prefix, remaining_params = replace_params(
@@ -446,7 +446,7 @@ class Mount(BaseRoute):
                     )
                 except NoMatchFound:
                     pass
-        raise NoMatchFound(__name, path_params)
+        raise NoMatchFound(name, path_params)
 
     async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
         await self.app(scope, receive, send)
@@ -493,8 +493,8 @@ class Host(BaseRoute):
                 return Match.FULL, child_scope
         return Match.NONE, {}
 
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
-        if self.name is not None and __name == self.name and "path" in path_params:
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
+        if self.name is not None and name == self.name and "path" in path_params:
             # 'name' matches "<mount_name>".
             path = path_params.pop("path")
             host, remaining_params = replace_params(
@@ -502,13 +502,13 @@ class Host(BaseRoute):
             )
             if not remaining_params:
                 return URLPath(path=path, host=host)
-        elif self.name is None or __name.startswith(self.name + ":"):
+        elif self.name is None or name.startswith(self.name + ":"):
             if self.name is None:
                 # No mount name.
-                remaining_name = __name
+                remaining_name = name
             else:
                 # 'name' matches "<mount_name>:<child_name>".
-                remaining_name = __name[len(self.name) + 1 :]
+                remaining_name = name[len(self.name) + 1 :]
             host, remaining_params = replace_params(
                 self.host_format, self.param_convertors, path_params
             )
@@ -518,7 +518,7 @@ class Host(BaseRoute):
                     return URLPath(path=str(url), protocol=url.protocol, host=host)
                 except NoMatchFound:
                     pass
-        raise NoMatchFound(__name, path_params)
+        raise NoMatchFound(name, path_params)
 
     async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
         await self.app(scope, receive, send)
@@ -652,13 +652,13 @@ class Router:
             response = PlainTextResponse("Not Found", status_code=404)
         await response(scope, receive, send)
 
-    def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
+    def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
         for route in self.routes:
             try:
-                return route.url_path_for(__name, **path_params)
+                return route.url_path_for(name, **path_params)
             except NoMatchFound:
                 pass
-        raise NoMatchFound(__name, path_params)
+        raise NoMatchFound(name, path_params)
 
     async def startup(self) -> None:
         """
index abc845fed3717098d74e67a5f1036d1fc0f0a832..ffa4133b81464a7a02896fae23d7a209d12fb3f9 100644 (file)
@@ -123,11 +123,9 @@ class Jinja2Templates:
         **env_options: typing.Any,
     ) -> "jinja2.Environment":
         @pass_context
-        # TODO: Make `__name` a positional-only argument when we drop Python 3.7
-        # support.
-        def url_for(context: dict, __name: str, **path_params: typing.Any) -> URL:
+        def url_for(context: dict, name: str, /, **path_params: typing.Any) -> URL:
             request = context["request"]
-            return request.url_for(__name, **path_params)
+            return request.url_for(name, **path_params)
 
         loader = jinja2.FileSystemLoader(directory)
         env_options.setdefault("loader", loader)
index 099edc3ff5845e7e9969781c1c46a2689110ee12..1b4f1303f96dd58a14b9dddeb39fdf76c048990b 100644 (file)
@@ -4,7 +4,6 @@ import io
 import json
 import math
 import queue
-import sys
 import typing
 import warnings
 from concurrent.futures import Future
@@ -27,12 +26,6 @@ except ModuleNotFoundError:  # pragma: no cover
         "You can install this with:\n"
         "    $ pip install httpx\n"
     )
-
-if sys.version_info >= (3, 8):  # pragma: no cover
-    from typing import TypedDict
-else:  # pragma: no cover
-    from typing_extensions import TypedDict
-
 _PortalFactoryType = typing.Callable[
     [], typing.ContextManager[anyio.abc.BlockingPortal]
 ]
@@ -64,7 +57,7 @@ class _WrapASGI2:
         await instance(receive, send)
 
 
-class _AsyncBackend(TypedDict):
+class _AsyncBackend(typing.TypedDict):
     backend: str
     backend_options: typing.Dict[str, typing.Any]
 
index 04f425cadcb71e3eb1ac7b744fdf8d325f594969..24f2bf7d734d4c8a806b52e7f8eeb1b2f11ab7da 100644 (file)
@@ -1,14 +1,8 @@
 import contextlib
 import functools
-import sys
 import typing
 import uuid
 
-if sys.version_info < (3, 8):
-    from typing_extensions import TypedDict  # pragma: no cover
-else:
-    from typing import TypedDict  # pragma: no cover
-
 import pytest
 
 from starlette.applications import Starlette
@@ -751,7 +745,7 @@ def test_lifespan_state_async_cm(test_client_factory):
     startup_complete = False
     shutdown_complete = False
 
-    class State(TypedDict):
+    class State(typing.TypedDict):
         count: int
         items: typing.List[int]