Skip to content

Commit

Permalink
Drop support for Python 3.7 (#2178)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kludex authored Jul 13, 2023
1 parent 12d9659 commit 5d12b86
Show file tree
Hide file tree
Showing 14 changed files with 44 additions and 110 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" },
]
Expand All @@ -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",
Expand Down
35 changes: 5 additions & 30 deletions starlette/_utils.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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

Expand Down
5 changes: 2 additions & 3 deletions starlette/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,8 @@ def build_middleware_stack(self) -> ASGIApp:
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
Expand Down
8 changes: 1 addition & 7 deletions starlette/middleware/sessions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
import sys
import typing
from base64 import b64decode, b64encode

Expand All @@ -10,11 +9,6 @@
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__(
Expand All @@ -24,7 +18,7 @@ def __init__(
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
Expand Down
4 changes: 2 additions & 2 deletions starlette/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ def state(self) -> State:
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)


Expand Down
24 changes: 3 additions & 21 deletions starlette/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
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
Expand All @@ -18,23 +17,6 @@
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
Expand Down Expand Up @@ -111,7 +93,7 @@ def set_cookie(
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
Expand Down Expand Up @@ -147,7 +129,7 @@ def delete_cookie(
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,
Expand Down
44 changes: 22 additions & 22 deletions starlette/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -258,12 +258,12 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
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
Expand Down Expand Up @@ -333,12 +333,12 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
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
Expand Down Expand Up @@ -415,22 +415,22 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
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(
self.path_format, self.param_convertors, path_params
)
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(
Expand All @@ -446,7 +446,7 @@ def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
)
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)
Expand Down Expand Up @@ -493,22 +493,22 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
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(
self.host_format, self.param_convertors, path_params
)
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
)
Expand All @@ -518,7 +518,7 @@ def url_path_for(self, __name: str, **path_params: typing.Any) -> URLPath:
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)
Expand Down Expand Up @@ -652,13 +652,13 @@ async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None:
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:
"""
Expand Down
6 changes: 2 additions & 4 deletions starlette/templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,9 @@ def _create_env(
**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)
Expand Down
9 changes: 1 addition & 8 deletions starlette/testclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import json
import math
import queue
import sys
import typing
import warnings
from concurrent.futures import Future
Expand All @@ -27,12 +26,6 @@
"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]
]
Expand Down Expand Up @@ -64,7 +57,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await instance(receive, send)


class _AsyncBackend(TypedDict):
class _AsyncBackend(typing.TypedDict):
backend: str
backend_options: typing.Dict[str, typing.Any]

Expand Down
Loading

0 comments on commit 5d12b86

Please sign in to comment.