Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
- {python: '3.12'}
- {python: '3.11'}
- {python: '3.10'}
- {python: '3.9'}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Dependencies
Quart dependends on the following packages, which will automatically
be installed with Quart:

- aiofiles, to load files in an asyncio compatible manner,
- AnyIO, to handle I/O operations,
- blinker, to manage signals,
- click, to manage command line arguments
- hypercorn, an ASGI server for development,
Expand Down
3 changes: 3 additions & 0 deletions examples/api/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import pytest
from api import app
from api import TodoIn


@pytest.mark.anyio
async def test_echo() -> None:
test_client = app.test_client()
response = await test_client.post("/echo", json={"a": "b"})
data = await response.get_json()
assert data == {"extra": True, "input": {"a": "b"}}


@pytest.mark.anyio
async def test_create_todo() -> None:
test_client = app.test_client()
response = await test_client.post("/todos/", json=TodoIn(task="Abc", due=None))
Expand Down
2 changes: 2 additions & 0 deletions examples/blog/tests/test_blog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from blog import app


@pytest.mark.anyio
async def test_create_post():
test_client = app.test_client()
response = await test_client.post(
Expand Down
2 changes: 2 additions & 0 deletions examples/chat/tests/test_chat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio

import pytest
from chat import app

from quart.testing.connections import (
Expand All @@ -11,6 +12,7 @@ async def _receive(test_websocket: _TestWebsocketConnection) -> str:
return await test_websocket.receive()


@pytest.mark.anyio
async def test_websocket() -> None:
test_client = app.test_client()
async with test_client.websocket("/ws") as test_websocket:
Expand Down
2 changes: 2 additions & 0 deletions examples/video/tests/test_video.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from video import app


@pytest.mark.anyio
async def test_auto_video() -> None:
test_client = app.test_client()
response = await test_client.get("/video.mp4")
Expand Down
15 changes: 6 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Typing :: Typed",
]
requires-python = ">=3.9"
requires-python = ">=3.10"
dependencies = [
"aiofiles",
"anyio>=4.14.0,<5",
"blinker>=1.6",
"click>=8.0",
"flask>=3.0",
Expand Down Expand Up @@ -48,6 +48,7 @@ quart = "quart.cli:main"

[dependency-groups]
dev = [
"trio==0.33.0",
"ruff",
"tox",
"tox-uv",
Expand All @@ -70,7 +71,6 @@ pre-commit = [
tests = [
"hypothesis",
"pytest",
"pytest-asyncio",
"pytest-cov",
"pytest-sugar",
"python-dotenv",
Expand All @@ -79,7 +79,6 @@ typing = [
"mypy",
"pyright",
"pytest",
"types-aiofiles",
]

[build-system]
Expand All @@ -94,8 +93,6 @@ default-groups = ["dev", "pre-commit", "tests", "typing"]

[tool.pytest.ini_options]
addopts = "--no-cov-on-fail --showlocals --strict-markers"
asyncio_default_fixture_loop_scope = "session"
asyncio_mode = "auto"
testpaths = ["tests"]
filterwarnings = [
"error",
Expand All @@ -116,7 +113,7 @@ exclude_also = [
]

[tool.mypy]
python_version = "3.9"
python_version = "3.10"
files = ["src", "tests"]
show_error_codes = true
pretty = true
Expand All @@ -131,7 +128,7 @@ strict_optional = false
warn_return_any = false

[tool.pyright]
pythonVersion = "3.9"
pythonVersion = "3.10"
include = ["src", "tests"]
typeCheckingMode = "basic"

Expand Down Expand Up @@ -162,7 +159,7 @@ order-by-type = false

[tool.tox]
env_list = [
"py3.13", "py3.12", "py3.11", "py3.10", "py3.9",
"py3.13", "py3.12", "py3.11", "py3.10",
"style",
"typing",
"docs",
Expand Down
24 changes: 11 additions & 13 deletions src/quart/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections import defaultdict
from collections.abc import AsyncGenerator
from collections.abc import Awaitable
from collections.abc import Callable
from collections.abc import Coroutine
from datetime import timedelta
from inspect import isasyncgen
Expand All @@ -16,16 +17,17 @@
from types import TracebackType
from typing import Any
from typing import AnyStr
from typing import Callable
from typing import cast
from typing import NoReturn
from typing import Optional
from typing import overload
from typing import ParamSpec
from typing import TypeVar
from urllib.parse import quote

from aiofiles import open as async_open
from aiofiles.base import AiofilesContextManager
import anyio
from anyio import AsyncFile
from anyio import open_file as async_open
from flask.sansio.app import App
from flask.sansio.scaffold import setupmethod
from hypercorn.asyncio import serve
Expand Down Expand Up @@ -125,11 +127,6 @@
from .wrappers import Response
from .wrappers import Websocket

if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec

# Python 3.14 deprecated asyncio.iscoroutinefunction, but suggested
# inspect.iscoroutinefunction does not work correctly in some Python
# versions before 3.12.
Expand Down Expand Up @@ -225,9 +222,9 @@ class Quart(App):
asgi_lifespan_class = ASGILifespan
asgi_websocket_class = ASGIWebsocketConnection
config_class = Config
event_class = asyncio.Event
event_class = anyio.Event
jinja_environment = Environment # type: ignore[assignment]
lock_class = asyncio.Lock
lock_class = anyio.Lock
request_class = Request
response_class = Response
session_interface = SecureCookieSessionInterface()
Expand Down Expand Up @@ -332,7 +329,8 @@ def __init__(
self.after_websocket_funcs: dict[
AppOrBlueprintKey, list[AfterWebsocketCallable]
] = defaultdict(list)
self.background_tasks: set[asyncio.Task] = set()
self.background_tasks: set[anyio.TaskHandle] = set()
self.before_serving_funcs: list[Callable[[], Awaitable[None]]] = []
self.before_serving_funcs: list[Callable[[], Awaitable[None]]] = []
self.before_websocket_funcs: dict[
AppOrBlueprintKey, list[BeforeWebsocketCallable]
Expand Down Expand Up @@ -392,7 +390,7 @@ async def open_resource(
self,
path: FilePath,
mode: str = "rb",
) -> AiofilesContextManager:
) -> AsyncFile:
"""Open a file for reading.

Use as
Expand All @@ -409,7 +407,7 @@ async def open_resource(

async def open_instance_resource(
self, path: FilePath, mode: str = "rb"
) -> AiofilesContextManager:
) -> AsyncFile:
"""Open a file for reading.

Use as
Expand Down
3 changes: 1 addition & 2 deletions src/quart/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from functools import partial
from typing import AnyStr
from typing import cast
from typing import Optional
from typing import TYPE_CHECKING
from urllib.parse import urlparse

Expand Down Expand Up @@ -110,7 +109,7 @@ async def handle_request(self, request: Request, send: ASGISendCallable) -> None
response = await _handle_exception(self.app, error)

if isinstance(response, Response) and response.timeout != Ellipsis:
timeout = cast(Optional[float], response.timeout)
timeout = cast(float | None, response.timeout)
else:
timeout = self.app.config["RESPONSE_TIMEOUT"]
try:
Expand Down
6 changes: 3 additions & 3 deletions src/quart/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from collections import defaultdict
from datetime import timedelta

from aiofiles import open as async_open
from aiofiles.base import AiofilesContextManager
from anyio import AsyncFile
from anyio import open_file as async_open
from flask.sansio.app import App
from flask.sansio.blueprints import Blueprint as SansioBlueprint # noqa
from flask.sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa
Expand Down Expand Up @@ -96,7 +96,7 @@ async def open_resource(
self,
path: FilePath,
mode: str = "rb",
) -> AiofilesContextManager:
) -> AsyncFile:
"""Open a file for reading.

Use as
Expand Down
12 changes: 2 additions & 10 deletions src/quart/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
import re
import sys
import traceback
from collections.abc import Callable
from importlib import import_module
from importlib.metadata import entry_points
from operator import attrgetter
from types import ModuleType
from typing import Any
from typing import Callable
from typing import TYPE_CHECKING

import click
Expand Down Expand Up @@ -488,15 +489,6 @@ def __init__(
def _load_plugin_commands(self) -> None:
if self._loaded_plugin_commands:
return

if sys.version_info >= (3, 10):
from importlib.metadata import entry_points
else:
# Use a backport on Python < 3.10. We technically have
# importlib.metadata on 3.8+, but the API changed in 3.10,
# so use the backport for consistency.
from importlib_metadata import entry_points

for point in entry_points(group="quart.commands"):
self.add_command(point.load(), point.name)

Expand Down
2 changes: 1 addition & 1 deletion src/quart/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import json
from collections.abc import Callable
from typing import Any
from typing import Callable

from flask.config import Config as FlaskConfig # noqa: F401
from flask.config import ConfigAttribute as ConfigAttribute # noqa: F401
Expand Down
2 changes: 1 addition & 1 deletion src/quart/ctx.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import annotations

import sys
from collections.abc import Callable
from contextvars import Token
from functools import wraps
from types import TracebackType
from typing import Any
from typing import Callable
from typing import cast
from typing import TYPE_CHECKING

Expand Down
8 changes: 4 additions & 4 deletions src/quart/datastructures.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations

from os import PathLike
from pathlib import Path
from typing import IO

from aiofiles import open as async_open
from anyio import open_file as async_open
from anyio import Path
from werkzeug.datastructures import FileStorage as WerkzeugFileStorage
from werkzeug.datastructures import Headers

Expand All @@ -30,7 +30,7 @@ async def save(self, destination: PathLike, buffer_size: int = 16384) -> None:
destination: A filename (str) or file object to write to.
buffer_size: Buffer size to keep in memory.
"""
async with async_open(destination, "wb") as file_:
async with await async_open(destination, "wb") as file_:
data = self.stream.read(buffer_size)
while data != b"":
await file_.write(data)
Expand All @@ -39,7 +39,7 @@ async def save(self, destination: PathLike, buffer_size: int = 16384) -> None:
async def load(self, source: PathLike, buffer_size: int = 16384) -> None:
path = Path(source)
self.filename = path.name
async with async_open(path, "rb") as file_:
async with await async_open(path, "rb") as file_:
data = await file_.read(buffer_size)
while data != b"":
self.stream.write(data)
Expand Down
7 changes: 3 additions & 4 deletions src/quart/formparser.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import annotations

from collections.abc import Awaitable
from collections.abc import Callable
from typing import Any
from typing import Callable
from typing import cast
from typing import IO
from typing import NoReturn
from typing import Optional
from typing import TYPE_CHECKING
from urllib.parse import parse_qsl

Expand All @@ -28,12 +27,12 @@
from .wrappers.request import Body

StreamFactory = Callable[
[Optional[int], Optional[str], Optional[str], Optional[int]],
[int | None, str | None, str | None, int | None],
IO[bytes],
]

ParserFunc = Callable[
["FormDataParser", "Body", str, Optional[int], dict[str, str]],
["FormDataParser", "Body", str, int | None, dict[str, str]],
Awaitable[tuple[MultiDict, MultiDict]],
]

Expand Down
2 changes: 1 addition & 1 deletion src/quart/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import pkgutil
import sys
from collections.abc import Callable
from collections.abc import Iterable
from datetime import datetime
from datetime import timedelta
Expand All @@ -13,7 +14,6 @@
from io import BytesIO
from pathlib import Path
from typing import Any
from typing import Callable
from typing import cast
from typing import NoReturn
from zlib import adler32
Expand Down
Loading
Loading