Skip to content

Commit 327c74b

Browse files
committed
Drop Python 3.9 and support 3.14
1 parent 77411ad commit 327c74b

File tree

9 files changed

+51
-60
lines changed

9 files changed

+51
-60
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ jobs:
1414
fail-fast: false
1515
matrix:
1616
include:
17+
- {name: '3.14', python: '3.14', tox: py314}
1718
- {name: '3.13', python: '3.13', tox: py313}
1819
- {name: '3.12', python: '3.12', tox: py312}
1920
- {name: '3.11', python: '3.11', tox: py311}
2021
- {name: '3.10', python: '3.10', tox: py310}
21-
- {name: '3.9', python: '3.9', tox: py39}
22-
- {name: 'format', python: '3.13', tox: format}
23-
- {name: 'mypy', python: '3.13', tox: mypy}
24-
- {name: 'pep8', python: '3.13', tox: pep8}
25-
- {name: 'package', python: '3.13', tox: package}
22+
- {name: 'format', python: '3.14', tox: format}
23+
- {name: 'mypy', python: '3.14', tox: mypy}
24+
- {name: 'pep8', python: '3.14', tox: pep8}
25+
- {name: 'package', python: '3.14', tox: package}
2626

2727
steps:
2828
- uses: actions/checkout@v4

.github/workflows/publish.yml

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

1212
- uses: actions/setup-python@v5
1313
with:
14-
python-version: 3.13
14+
python-version: 3.14
1515

1616
- run: |
1717
pip install pdm

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2
33
build:
44
os: ubuntu-24.04
55
tools:
6-
python: "3.13"
6+
python: "3.14"
77

88
python:
99
install:

docs/tutorials/installation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Installation
44
============
55

6-
Quart-Auth is only compatible with Python 3.9 or higher and can be
6+
Quart-Auth is only compatible with Python 3.10 or higher and can be
77
installed using pip or your favorite python package manager.
88

99
.. code-block:: sh

pyproject.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ classifiers = [
1313
"Operating System :: OS Independent",
1414
"Programming Language :: Python",
1515
"Programming Language :: Python :: 3",
16-
"Programming Language :: Python :: 3.9",
1716
"Programming Language :: Python :: 3.10",
1817
"Programming Language :: Python :: 3.11",
1918
"Programming Language :: Python :: 3.12",
2019
"Programming Language :: Python :: 3.13",
20+
"Programming Language :: Python :: 3.14",
2121
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
2222
"Topic :: Software Development :: Libraries :: Python Modules",
2323
]
@@ -27,10 +27,8 @@ readme = "README.rst"
2727
repository = "https://github.com/pgjones/quart-auth/"
2828
dependencies = [
2929
"quart >= 0.18",
30-
"typing_extensions; python_version < '3.10'",
31-
3230
]
33-
requires-python = ">=3.9"
31+
requires-python = ">=3.10"
3432

3533
[project.optional-dependencies]
3634
docs = ["pydata_sphinx_theme"]

src/quart_auth/basic_auth.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1+
from collections.abc import Awaitable, Callable
12
from functools import wraps
23
from secrets import compare_digest
3-
from typing import Awaitable, Callable, TypeVar
4+
from typing import ParamSpec, TypeVar
45

56
from quart import current_app, has_request_context, has_websocket_context, request, websocket
67
from werkzeug.datastructures import WWWAuthenticate
78
from werkzeug.exceptions import Unauthorized as WerkzeugUnauthorized
89

9-
try:
10-
from typing import ParamSpec
11-
except ImportError:
12-
from typing_extensions import ParamSpec
13-
1410
T = TypeVar("T")
1511
P = ParamSpec("P")
1612

src/quart_auth/extension.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import warnings
2+
from collections.abc import AsyncGenerator, Iterable
23
from contextlib import asynccontextmanager
34
from enum import auto, Enum
45
from hashlib import sha512
5-
from typing import Any, AsyncGenerator, cast, Dict, Iterable, Literal, Optional, Type, Union
6+
from typing import Any, Literal
67

78
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
89
from quart import (
@@ -47,7 +48,7 @@ class Action(Enum):
4748

4849
class _AuthSerializer(URLSafeTimedSerializer):
4950
def __init__(
50-
self, secret: Union[str, bytes, Iterable[str], Iterable[bytes]], salt: Union[str, bytes]
51+
self, secret: str | bytes | Iterable[str] | Iterable[bytes], salt: str | bytes
5152
) -> None:
5253
super().__init__(secret, salt, signer_kwargs={"digest_method": sha512})
5354

@@ -59,12 +60,12 @@ class AuthUser:
5960
inherit from this.
6061
"""
6162

62-
def __init__(self, auth_id: Optional[str], action: Action = Action.PASS) -> None:
63+
def __init__(self, auth_id: str | None, action: Action = Action.PASS) -> None:
6364
self._auth_id = auth_id
6465
self.action = action
6566

6667
@property
67-
def auth_id(self) -> Optional[str]:
68+
def auth_id(self) -> str | None:
6869
return self._auth_id
6970

7071
@property
@@ -81,21 +82,21 @@ class QuartAuth:
8182

8283
def __init__(
8384
self,
84-
app: Optional[Quart] = None,
85+
app: Quart | None = None,
8586
*,
8687
attribute_name: str = None,
87-
cookie_domain: Optional[str] = None,
88-
cookie_name: Optional[str] = None,
89-
cookie_path: Optional[str] = None,
90-
cookie_http_only: Optional[bool] = None,
91-
cookie_samesite: Optional[Literal["Strict", "Lax"]] = None,
92-
cookie_secure: Optional[bool] = None,
93-
duration: Optional[int] = None,
94-
mode: Optional[Literal["cookie", "bearer"]] = None,
95-
salt: Optional[str] = None,
88+
cookie_domain: str | None = None,
89+
cookie_name: str | None = None,
90+
cookie_path: str | None = None,
91+
cookie_http_only: bool | None = None,
92+
cookie_samesite: Literal["Strict", "Lax"] | None = None,
93+
cookie_secure: bool | None = None,
94+
duration: int | None = None,
95+
mode: Literal["cookie", "bearer"] | None = None,
96+
salt: str | None = None,
9697
singleton: bool = True,
97-
serializer_class: Optional[Type[_AuthSerializer]] = None,
98-
user_class: Optional[Type[AuthUser]] = None,
98+
serializer_class: type[_AuthSerializer] | None = None,
99+
user_class: type[AuthUser] | None = None,
99100
) -> None:
100101
self.attribute_name = attribute_name
101102
self.cookie_domain = cookie_domain
@@ -168,7 +169,7 @@ def resolve_user(self) -> AuthUser:
168169

169170
return self.user_class(auth_id)
170171

171-
def load_cookie(self) -> Optional[str]:
172+
def load_cookie(self) -> str | None:
172173
try:
173174
token = ""
174175
if has_request_context():
@@ -180,7 +181,7 @@ def load_cookie(self) -> Optional[str]:
180181
else:
181182
return self.load_token(token)
182183

183-
def load_bearer(self) -> Optional[str]:
184+
def load_bearer(self) -> str | None:
184185
try:
185186
if has_request_context():
186187
raw = request.headers["Authorization"]
@@ -194,14 +195,14 @@ def load_bearer(self) -> Optional[str]:
194195
token = raw[6:].strip()
195196
return self.load_token(token)
196197

197-
def dump_token(self, auth_id: str, app: Optional[Quart] = None) -> str:
198+
def dump_token(self, auth_id: str, app: Quart | None = None) -> str:
198199
if app is None:
199200
app = current_app
200201

201202
serializer = self.serializer_class(app.secret_key, self.salt)
202203
return serializer.dumps(auth_id)
203204

204-
def load_token(self, token: str, app: Optional[Quart] = None) -> Optional[str]:
205+
def load_token(self, token: str, app: Quart | None = None) -> str | None:
205206
if app is None:
206207
app = current_app
207208

@@ -212,7 +213,7 @@ def load_token(self, token: str, app: Optional[Quart] = None) -> Optional[str]:
212213

213214
keys.append(app.secret_key) # itsdangerous expects current key at top
214215

215-
serializer = self.serializer_class(keys, self.salt) # type: ignore[arg-type]
216+
serializer = self.serializer_class(keys, self.salt)
216217
try:
217218
return serializer.loads(token, max_age=self.duration)
218219
except (BadSignature, SignatureExpired):
@@ -230,9 +231,9 @@ async def after_request(self, response: Response) -> Response:
230231
response.delete_cookie(
231232
self.cookie_name,
232233
domain=self.cookie_domain,
233-
httponly=cast(bool, self.cookie_http_only),
234+
httponly=self.cookie_http_only,
234235
path=self.cookie_path,
235-
secure=cast(bool, self.cookie_secure),
236+
secure=self.cookie_secure,
236237
samesite=self.cookie_samesite,
237238
)
238239
elif user.action in {Action.WRITE, Action.WRITE_PERMANENT}:
@@ -252,14 +253,14 @@ async def after_request(self, response: Response) -> Response:
252253
token,
253254
domain=self.cookie_domain,
254255
max_age=max_age,
255-
httponly=cast(bool, self.cookie_http_only),
256+
httponly=self.cookie_http_only,
256257
path=self.cookie_path,
257-
secure=cast(bool, self.cookie_secure),
258+
secure=self.cookie_secure,
258259
samesite=self.cookie_samesite,
259260
)
260261
return response
261262

262-
async def after_websocket(self, response: Optional[Response]) -> Optional[Response]:
263+
async def after_websocket(self, response: Response | None) -> Response | None:
263264
user = self.load_user()
264265
if self.mode == "bearer":
265266
if user.action != Action.PASS:
@@ -330,8 +331,8 @@ async def authenticated_client(
330331
token,
331332
path=self.cookie_path,
332333
domain=self.cookie_domain,
333-
secure=cast(bool, self.cookie_secure),
334-
httponly=cast(bool, self.cookie_http_only),
334+
secure=self.cookie_secure,
335+
httponly=self.cookie_http_only,
335336
samesite=self.cookie_samesite,
336337
)
337338
yield
@@ -342,7 +343,7 @@ async def authenticated_client(
342343
domain=self.cookie_domain,
343344
)
344345

345-
def _template_context(self) -> Dict[str, AuthUser]:
346+
def _template_context(self) -> dict[str, AuthUser]:
346347
return {"current_user": self.load_user()}
347348

348349

src/quart_auth/globals.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1+
from collections.abc import AsyncGenerator, Awaitable, Callable
12
from contextlib import asynccontextmanager
23
from functools import wraps
3-
from typing import AsyncGenerator, Awaitable, Callable, Optional, TypeVar
4+
from typing import ParamSpec, TypeVar
45

56
from quart import current_app, Quart
67
from quart.typing import TestClientProtocol
78
from werkzeug.local import LocalProxy
89

910
from .extension import Action, AuthUser, QuartAuth, Unauthorized
1011

11-
try:
12-
from typing import ParamSpec
13-
except ImportError:
14-
from typing_extensions import ParamSpec
15-
1612
T = TypeVar("T")
1713
P = ParamSpec("P")
1814

@@ -24,7 +20,7 @@ def _load_user() -> AuthUser:
2420
current_user: AuthUser = LocalProxy(_load_user) # type: ignore
2521

2622

27-
def _find_extension(app: Optional[Quart] = None) -> QuartAuth:
23+
def _find_extension(app: Quart | None = None) -> QuartAuth:
2824
if app is None:
2925
app = current_app
3026
extension = next(

tox.ini

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = docs,format,mypy,py39,py310,py311,py312,py313,pep8,package
2+
envlist = docs,format,mypy,py310,py311,py312,py313,py314,pep8,package
33
isolated_build = True
44

55
[testenv]
@@ -11,7 +11,7 @@ deps =
1111
commands = pytest --cov=quart_auth {posargs}
1212

1313
[testenv:docs]
14-
basepython = python3.13
14+
basepython = python3.14
1515
deps =
1616
pydata-sphinx-theme
1717
sphinx<6
@@ -20,7 +20,7 @@ commands =
2020
sphinx-build -W --keep-going -b html -d {envtmpdir}/doctrees docs/ docs/_build/html/
2121

2222
[testenv:format]
23-
basepython = python3.13
23+
basepython = python3.14
2424
deps =
2525
black
2626
isort
@@ -29,23 +29,23 @@ commands =
2929
isort --check --diff src/quart_auth/ tests
3030

3131
[testenv:pep8]
32-
basepython = python3.13
32+
basepython = python3.14
3333
deps =
3434
flake8
3535
pep8-naming
3636
flake8-print
3737
commands = flake8 src/quart_auth/ tests/
3838

3939
[testenv:mypy]
40-
basepython = python3.13
40+
basepython = python3.14
4141
deps =
4242
mypy
4343
pytest
4444
commands =
4545
mypy src/quart_auth/ tests/
4646

4747
[testenv:package]
48-
basepython = python3.13
48+
basepython = python3.14
4949
deps =
5050
pdm
5151
twine

0 commit comments

Comments
 (0)