Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-quart-trio for openSUSE:Factory checked in at 2026-10-15 18:20:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-quart-trio (Old) and /work/SRC/openSUSE:Factory/.python-quart-trio.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-quart-trio" Thu Oct 15 18:20:00 2026 rev:4 rq:1384705 version:0.13.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-quart-trio/python-quart-trio.changes 2025-02-14 19:19:43.783794898 +0100 +++ /work/SRC/openSUSE:Factory/.python-quart-trio.new.3698736/python-quart-trio.changes 2026-10-15 18:20:01.499140492 +0200 @@ -1,0 +2,7 @@ +Wed Oct 14 04:26:33 UTC 2026 - Steve Kowalik <[email protected]> + +- Update to 0.13.0: + * Update to support Quart 0.23.0 + * Drop support for Python 3.12, 3.11, 3.10, 3.9. + +------------------------------------------------------------------- Old: ---- quart_trio-0.12.0.tar.gz New: ---- quart_trio-0.13.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-quart-trio.spec ++++++ --- /var/tmp/diff_new_pack.c4ty9R/_old 2026-10-15 18:20:02.414178803 +0200 +++ /var/tmp/diff_new_pack.c4ty9R/_new 2026-10-15 18:20:02.415178845 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-quart-trio # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,14 +18,14 @@ %{?sle15_python_module_pythons} Name: python-quart-trio -Version: 0.12.0 +Version: 0.13.0 Release: 0 Summary: A Quart extension to provide trio support License: MIT URL: https://github.com/pgjones/quart-trio/ Source: https://github.com/pgjones/quart-trio/archive/refs/tags/%{version}.tar.gz#/quart_trio-%{version}.tar.gz -BuildRequires: %{python_module Quart >= 0.19} -BuildRequires: %{python_module exceptiongroup} +BuildRequires: %{python_module Quart >= 0.23} +BuildRequires: %{python_module base >= 3.13} BuildRequires: %{python_module pdm-backend} BuildRequires: %{python_module pip} BuildRequires: %{python_module pytest-cov} @@ -34,8 +34,7 @@ BuildRequires: %{python_module trio >= 0.19} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-Quart >= 0.19 -Requires: python-exceptiongroup +Requires: python-Quart >= 0.23 Requires: python-hypercorn >= 0.12 Requires: python-trio >= 0.19 Recommends: python-python-dotenv @@ -58,8 +57,7 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -# Trio breakage with exceptiongroup handling -%pytest -k 'not (test_websocket_exception_group or test_websocket_abort)' +%pytest %files %{python_files} %doc README.rst ++++++ quart_trio-0.12.0.tar.gz -> quart_trio-0.13.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/.github/workflows/ci.yml new/quart-trio-0.13.0/.github/workflows/ci.yml --- old/quart-trio-0.12.0/.github/workflows/ci.yml 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/.github/workflows/ci.yml 2026-09-12 18:51:24.000000000 +0200 @@ -6,6 +6,8 @@ branches: [ main ] workflow_dispatch: +permissions: {} + jobs: tox: name: ${{ matrix.name }} @@ -17,18 +19,15 @@ fail-fast: false matrix: include: + - {name: '3.14', python: '3.14', tox: py313} - {name: '3.13', python: '3.13', tox: py313} - - {name: '3.12', python: '3.12', tox: py312} - - {name: '3.11', python: '3.11', tox: py311} - - {name: '3.10', python: '3.10', tox: py310} - - {name: '3.9', python: '3.9', tox: py39} - - {name: 'format', python: '3.13', tox: format} - - {name: 'mypy', python: '3.13', tox: mypy} - - {name: 'pep8', python: '3.13', tox: pep8} - - {name: 'package', python: '3.13', tox: package} + - {name: 'format', python: '3.14', tox: format} + - {name: 'mypy', python: '3.14', tox: mypy} + - {name: 'pep8', python: '3.14', tox: pep8} + - {name: 'package', python: '3.14', tox: package} steps: - - uses: pgjones/actions/tox@v1 + - uses: pgjones/actions/tox@64fb3fd1840712671bd6f792e5b803f53f63fbdf # v1.1 with: environment: ${{ matrix.tox }} @@ -37,4 +36,4 @@ runs-on: ubuntu-latest steps: - - uses: pgjones/actions/zizmor@v1 + - uses: pgjones/actions/zizmor@64fb3fd1840712671bd6f792e5b803f53f63fbdf # v1.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/.github/workflows/publish.yml new/quart-trio-0.13.0/.github/workflows/publish.yml --- old/quart-trio-0.12.0/.github/workflows/publish.yml 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/.github/workflows/publish.yml 2026-09-12 18:51:24.000000000 +0200 @@ -3,11 +3,14 @@ push: tags: - '*' + +permissions: {} + jobs: build: runs-on: ubuntu-latest steps: - - uses: pgjones/actions/build@v1 + - uses: pgjones/actions/build@64fb3fd1840712671bd6f792e5b803f53f63fbdf # v1.1 pypi-publish: needs: ['build'] @@ -19,9 +22,9 @@ # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: "./dist" - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: artifact/ + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/CHANGELOG.rst new/quart-trio-0.13.0/CHANGELOG.rst --- old/quart-trio-0.12.0/CHANGELOG.rst 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/CHANGELOG.rst 2026-09-12 18:51:24.000000000 +0200 @@ -1,3 +1,9 @@ +0.13.0 2026-09-12 +----------------- + +* Update to support Quart 0.23.0 +* Drop support for Python 3.12, 3.11, 3.10, 3.9. + 0.12.0 2025-01-09 ----------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/README.rst new/quart-trio-0.13.0/README.rst --- old/quart-trio-0.12.0/README.rst 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/README.rst 2026-09-12 18:51:24.000000000 +0200 @@ -19,7 +19,7 @@ $ pip install quart-trio -and requires Python 3.8 or higher (see `python version support +and requires Python 3.13 or higher (see `python version support <https://quart.palletsprojects.com/en/latest/discussion/python_versions.html>`_ for reasoning). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/docs/conf.py new/quart-trio-0.13.0/docs/conf.py --- old/quart-trio-0.12.0/docs/conf.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/docs/conf.py 2026-09-12 18:51:24.000000000 +0200 @@ -13,6 +13,7 @@ import os import sys from importlib.metadata import version as imp_version + sys.path.insert(0, os.path.abspath('.')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/docs/tutorials/installation.rst new/quart-trio-0.13.0/docs/tutorials/installation.rst --- old/quart-trio-0.12.0/docs/tutorials/installation.rst 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/docs/tutorials/installation.rst 2026-09-12 18:51:24.000000000 +0200 @@ -3,7 +3,7 @@ Installation ============ -Quart-Trio is only compatible with Python 3.9 or higher and can be +Quart-Trio is only compatible with Python 3.13 or higher and can be installed using pip or your favorite python package manager. .. code-block:: sh diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/pyproject.toml new/quart-trio-0.13.0/pyproject.toml --- old/quart-trio-0.12.0/pyproject.toml 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/pyproject.toml 2026-09-12 18:51:24.000000000 +0200 @@ -1,6 +1,6 @@ [project] name = "quart-trio" -version = "0.12.0" +version = "0.13.0" description = "A Quart extension to provide trio support" authors = [ {name = "pgjones", email = "[email protected]"}, @@ -12,12 +12,8 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -26,19 +22,18 @@ readme = "README.rst" repository = "https://github.com/pgjones/quart-trio/" dependencies = [ - "exceptiongroup >= 1.1.0; python_version < '3.11'", "hypercorn[trio] >= 0.12.0", - "quart >= 0.19", + "quart >= 0.23", "trio >= 0.19.0", ] -requires-python = ">=3.9" +requires-python = ">=3.13" [project.optional-dependencies] docs = ["pydata_sphinx_theme"] [tool.black] line-length = 100 -target-version = ["py39"] +target-version = ["py313"] [tool.isort] combine_as_imports = true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/app.py new/quart-trio-0.13.0/src/quart_trio/app.py --- old/quart-trio-0.12.0/src/quart_trio/app.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/app.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,32 +1,23 @@ import sys import warnings -from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar, Union +from typing import Any, Awaitable, Callable, Coroutine, Optional, ParamSpec, TypeVar, Union import trio from hypercorn.config import Config as HyperConfig from hypercorn.trio import serve from quart import Quart, request_started, websocket_started -from quart.ctx import RequestContext, WebsocketContext +from quart.ctx import AppContext from quart.signals import got_serving_exception -from quart.typing import FilePath, ResponseReturnValue +from quart.typing import FilePath, ResponseReturnValue, ResponseTypes from quart.utils import file_path_to_path -from quart.wrappers import Request, Response, Websocket +from quart.wrappers import Request, Websocket from werkzeug.exceptions import HTTPException -from werkzeug.wrappers import Response as WerkzeugResponse from .asgi import TrioASGIHTTPConnection, TrioASGILifespan, TrioASGIWebsocketConnection from .testing import TrioClient, TrioTestApp from .utils import run_sync from .wrappers import TrioRequest, TrioResponse, TrioWebsocket -try: - from typing import ParamSpec -except ImportError: - from typing_extensions import ParamSpec - -if sys.version_info < (3, 11): - from exceptiongroup import BaseExceptionGroup - T = TypeVar("T") P = ParamSpec("P") @@ -140,10 +131,10 @@ """ return run_sync(func) - async def handle_request(self, request: Request) -> Union[Response, WerkzeugResponse]: - async with self.request_context(request) as request_context: + async def handle_request(self, request: Request) -> ResponseTypes: + async with self.request_context(request) as ctx: try: - return await self.full_dispatch_request(request_context) + return await self.full_dispatch_request(ctx) except trio.Cancelled: raise # Cancelled should be handled by serving code. except BaseExceptionGroup as error: @@ -151,53 +142,48 @@ if filtered_error is not None: raise filtered_error - return await self.handle_exception(error) # type: ignore + return await self.handle_exception(ctx, error) # type: ignore except Exception as error: - return await self.handle_exception(error) + return await self.handle_exception(ctx, error) finally: if request.scope.get("_quart._preserve_context", False): - self._preserved_context = request_context.copy() + self._preserved_context = ctx.copy() - async def full_dispatch_request( - self, request_context: Optional[RequestContext] = None - ) -> Union[Response, WerkzeugResponse]: + async def full_dispatch_request(self, ctx: AppContext) -> ResponseTypes: """Adds pre and post processing to the request dispatching. Arguments: - request_context: The request context, optional as Flask - omits this argument. + request_context: The context containing the request. """ try: await request_started.send_async(self, _sync_wrapper=self.ensure_async) # type: ignore result: ResponseReturnValue | HTTPException | None - result = await self.preprocess_request(request_context) + result = await self.preprocess_request(ctx) if result is None: - result = await self.dispatch_request(request_context) + result = await self.dispatch_request(ctx) except (Exception, BaseExceptionGroup) as error: - result = await self.handle_user_exception(error) - return await self.finalize_request(result, request_context) + result = await self.handle_user_exception(ctx, error) + return await self.finalize_request(ctx, result) async def handle_user_exception( - self, error: Union[Exception, BaseExceptionGroup] + self, ctx: AppContext, error: Union[Exception, BaseExceptionGroup] ) -> Union[HTTPException, ResponseReturnValue]: if isinstance(error, BaseExceptionGroup): for exception in error.exceptions: try: - return await self.handle_user_exception(exception) # type: ignore + return await self.handle_user_exception(ctx, exception) # type: ignore except Exception: pass # No handler for this error # Not found a single handler, re-raise the error raise error else: - return await super().handle_user_exception(error) + return await super().handle_user_exception(ctx, error) - async def handle_websocket( - self, websocket: Websocket - ) -> Optional[Union[Response, WerkzeugResponse]]: - async with self.websocket_context(websocket) as websocket_context: + async def handle_websocket(self, websocket: Websocket) -> ResponseTypes | None: + async with self.websocket_context(websocket) as ctx: try: - return await self.full_dispatch_websocket(websocket_context) + return await self.full_dispatch_websocket(ctx) except trio.Cancelled: raise # Cancelled should be handled by serving code. except BaseExceptionGroup as error: @@ -205,21 +191,18 @@ if filtered_error is not None: raise filtered_error - return await self.handle_websocket_exception(error) # type: ignore + return await self.handle_websocket_exception(ctx, error) # type: ignore except Exception as error: - return await self.handle_websocket_exception(error) + return await self.handle_websocket_exception(ctx, error) finally: if websocket.scope.get("_quart._preserve_context", False): - self._preserved_context = websocket_context.copy() + self._preserved_context = ctx.copy() - async def full_dispatch_websocket( - self, websocket_context: Optional[WebsocketContext] = None - ) -> Optional[Union[Response, WerkzeugResponse]]: + async def full_dispatch_websocket(self, ctx: AppContext) -> ResponseTypes: """Adds pre and post processing to the websocket dispatching. Arguments: - websocket_context: The websocket context, optional to match - the Flask convention. + websocket_context: The context containing the websocket. """ try: await websocket_started.send_async( @@ -227,12 +210,12 @@ ) result: ResponseReturnValue | HTTPException | None - result = await self.preprocess_websocket(websocket_context) + result = await self.preprocess_websocket(ctx) if result is None: - result = await self.dispatch_websocket(websocket_context) + result = await self.dispatch_websocket(ctx) except (Exception, BaseExceptionGroup) as error: - result = await self.handle_user_exception(error) - return await self.finalize_websocket(result, websocket_context) + result = await self.handle_user_exception(ctx, error) + return await self.finalize_websocket(ctx, result) async def open_instance_resource( self, path: FilePath, mode: str = "rb" @@ -265,11 +248,11 @@ def add_background_task(self, func: Callable, *args: Any, **kwargs: Any) -> None: async def _wrapper() -> None: - try: - async with self.app_context(): + async with self.app_context() as ctx: + try: await self.ensure_async(func)(*args, **kwargs) - except (BaseExceptionGroup, Exception) as error: - await self.handle_background_exception(error) # type: ignore + except (BaseExceptionGroup, Exception) as error: + await self.handle_background_exception(ctx, error) # type: ignore self.nursery.start_soon(_wrapper) @@ -281,20 +264,20 @@ else: self.nursery.cancel_scope.cancel() - try: - async with self.app_context(): - for func in self.after_serving_funcs: - await self.ensure_async(func)() - for gen in self.while_serving_gens: + async with self.app_context() as ctx: + try: + for gen in reversed(self.while_serving_gens): try: await gen.__anext__() except StopAsyncIteration: pass else: raise RuntimeError("While serving generator didn't terminate") - except Exception as error: - await got_serving_exception.send_async( - self, _sync_wrapper=self.ensure_async, exception=error # type: ignore - ) - self.log_exception(sys.exc_info()) - raise + for func in self.after_serving_funcs: + await self.ensure_async(func)() + except Exception as error: + await got_serving_exception.send_async( + self, _sync_wrapper=self.ensure_async, exception=error # type: ignore + ) + self.log_exception(ctx, sys.exc_info()) + raise diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/asgi.py new/quart-trio-0.13.0/src/quart_trio/asgi.py --- old/quart-trio-0.12.0/src/quart_trio/asgi.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/asgi.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,7 +1,4 @@ -import sys -from functools import partial -from typing import cast, Optional, TYPE_CHECKING, Union -from urllib.parse import urlparse +from typing import cast, Optional, TYPE_CHECKING import trio from hypercorn.typing import ( @@ -12,22 +9,18 @@ LifespanShutdownFailedEvent, LifespanStartupCompleteEvent, LifespanStartupFailedEvent, - WebsocketScope, ) -from quart.asgi import ASGIHTTPConnection, ASGIWebsocketConnection +from quart.asgi import _convert_os_error, ASGIHTTPConnection, ASGIWebsocketConnection from quart.signals import websocket_received from quart.wrappers import Request, Response, Websocket # noqa: F401 -from werkzeug.datastructures import Headers if TYPE_CHECKING: from quart_trio import QuartTrio # noqa: F401 -if sys.version_info < (3, 11): - from exceptiongroup import BaseExceptionGroup - class TrioASGIHTTPConnection(ASGIHTTPConnection): async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + send = _convert_os_error(send) request = self._create_request_from_scope(send) async with trio.open_nursery() as nursery: nursery.start_soon(self.handle_messages, nursery, request, receive) @@ -36,8 +29,17 @@ async def handle_messages( # type: ignore self, nursery: trio.Nursery, request: Request, receive: ASGIReceiveCallable ) -> None: - await super().handle_messages(request, receive) - nursery.cancel_scope.cancel() + while True: + message = await receive() + if message["type"] == "http.request": + await request.body.put(message.get("body", b"")) + if not message.get("more_body", False): + request.body.set_complete() + elif message["type"] == "http.disconnect": + self._disconnected = True + request.body.disconnect() + nursery.cancel_scope.cancel() + return async def handle_request( # type: ignore self, nursery: trio.Nursery, request: Request, send: ASGISendCallable @@ -57,21 +59,15 @@ class TrioASGIWebsocketConnection(ASGIWebsocketConnection): - def __init__(self, app: "QuartTrio", scope: WebsocketScope) -> None: - self.app = app - self.scope = scope - self._accepted = False - self._closed = False - self.send_channel, self.receive_channel = trio.open_memory_channel[Union[bytes, str]](10) - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + send = _convert_os_error(send) websocket = self._create_websocket_from_scope(send) async with trio.open_nursery() as nursery: - nursery.start_soon(self.handle_messages, nursery, receive) + nursery.start_soon(self.handle_messages, nursery, websocket, receive) nursery.start_soon(self.handle_websocket, nursery, websocket, send) async def handle_messages( # type: ignore - self, nursery: trio.Nursery, receive: ASGIReceiveCallable + self, nursery: trio.Nursery, websocket: Websocket, receive: ASGIReceiveCallable ) -> None: while True: event = await receive() @@ -80,42 +76,13 @@ await websocket_received.send_async( message, _sync_wrapper=self.app.ensure_async # type: ignore ) - await self.send_channel.send(message) + await websocket.buffer.put(message) elif event["type"] == "websocket.disconnect": + self._disconnected = True + websocket.buffer.disconnect() break nursery.cancel_scope.cancel() - def _create_websocket_from_scope(self, send: ASGISendCallable) -> Websocket: - headers = Headers() - headers["Remote-Addr"] = (self.scope.get("client") or ["<local>"])[0] - for name, value in self.scope["headers"]: - headers.add(name.decode("latin1").title(), value.decode("latin1")) - - path = self.scope["path"] - path = path if path[0] == "/" else urlparse(path).path - root_path = self.scope.get("root_path", "") - if root_path != "": - try: - path = path.split(root_path, 1)[1] - path = " " if path == "" else path - except IndexError: - path = " " # Invalid in paths, hence will result in 404 - - return self.app.websocket_class( - path, - self.scope["query_string"], - self.scope["scheme"], - headers, - self.scope.get("root_path", ""), - self.scope.get("http_version", "1.1"), - list(self.scope.get("subprotocols", [])), - self.receive_channel.receive, - partial(self.send_data, send), - partial(self.accept_connection, send), - partial(self.close_connection, send), - scope=self.scope, - ) - async def handle_websocket( # type: ignore self, nursery: trio.Nursery, websocket: Websocket, send: ASGISendCallable ) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/datastructures.py new/quart-trio-0.13.0/src/quart_trio/datastructures.py --- old/quart-trio-0.12.0/src/quart_trio/datastructures.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/datastructures.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,10 +1,54 @@ from __future__ import annotations +from asyncio import QueueEmpty, QueueShutDown from os import PathLike +from typing import Generic, TypeVar +import trio from quart.datastructures import FileStorage from trio import open_file, Path, wrap_file +T = TypeVar("T") + + +class TrioQueue(Generic[T]): + """Queue operations used by Quart's body and websocket buffers.""" + + def __init__(self) -> None: + self._send, self._receive = trio.open_memory_channel[T](float("inf")) + + async def put(self, value: T) -> None: + try: + await self._send.send(value) + except trio.ClosedResourceError: + raise QueueShutDown from None + + def put_nowait(self, value: T) -> None: + try: + self._send.send_nowait(value) + except trio.ClosedResourceError: + raise QueueShutDown from None + + async def get(self) -> T: + try: + return await self._receive.receive() + except trio.EndOfChannel: + raise QueueShutDown from None + + def get_nowait(self) -> T: + try: + return self._receive.receive_nowait() + except trio.WouldBlock: + raise QueueEmpty from None + except trio.EndOfChannel: + raise QueueShutDown from None + + def empty(self) -> bool: + return self._receive.statistics().current_buffer_used == 0 + + def shutdown(self) -> None: + self._send.close() + class TrioFileStorage(FileStorage): async def save(self, destination: PathLike, buffer_size: int = 16384) -> None: # type: ignore diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/testing/connections.py new/quart-trio-0.13.0/src/quart_trio/testing/connections.py --- old/quart-trio-0.12.0/src/quart_trio/testing/connections.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/testing/connections.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,20 +1,17 @@ from __future__ import annotations from types import TracebackType -from typing import Any, AnyStr, Awaitable, List, Optional, Tuple, Union +from typing import Any, Awaitable, List, Optional, Tuple, Union import trio from hypercorn.typing import HTTPScope, WebsocketScope from quart.app import Quart from quart.json import dumps, loads -from quart.testing.connections import ( - HTTPDisconnectError, - WebsocketDisconnectError, - WebsocketResponseError, -) +from quart.testing.connections import WebsocketResponseError from quart.typing import TestHTTPConnectionProtocol, TestWebsocketConnectionProtocol from quart.utils import decode_headers from quart.wrappers import Response +from quart.wrappers.base import ClientDisconnectedError from werkzeug.datastructures import Headers @@ -64,7 +61,7 @@ async for data in self._client_receive: if isinstance(data, bytes): self.response_data.extend(data) - elif not isinstance(data, HTTPDisconnectError): + else: raise data except trio.ClosedResourceError: pass @@ -92,9 +89,6 @@ await self._client_send.aclose() elif message["type"] == "http.response.push": self.push_promises.append((message["path"], decode_headers(message["headers"]))) - elif message["type"] == "http.disconnect": - await self._client_send.send(HTTPDisconnectError()) - await self._client_send.aclose() class TestWebsocketConnection: @@ -121,14 +115,14 @@ await self.disconnect() await self._nursery_manager.__aexit__(exc_type, exc_value, tb) - async def receive(self) -> AnyStr: + async def receive(self) -> str | bytes: data = await self._client_receive.receive() if isinstance(data, Exception): raise data else: - return data # type: ignore + return data - async def send(self, data: AnyStr) -> None: + async def send(self, data: str | bytes) -> None: if isinstance(data, str): await self._server_send.send({"type": "websocket.receive", "text": data}) else: @@ -172,5 +166,5 @@ ) ) elif message["type"] == "websocket.close": - await self._client_send.send(WebsocketDisconnectError(message.get("code", 1000))) + await self._client_send.send(ClientDisconnectedError(message.get("code", 1000))) await self._client_send.aclose() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/utils.py new/quart-trio-0.13.0/src/quart_trio/utils.py --- old/quart-trio-0.12.0/src/quart_trio/utils.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/utils.py 2026-09-12 18:51:24.000000000 +0200 @@ -18,7 +18,7 @@ async def _wrapper(*args: Any, **kwargs: Any) -> Any: result = await trio.to_thread.run_sync(copy_context().run, partial(func, *args, **kwargs)) if isgenerator(result): - return run_sync_iterable(result) + return run_sync_iterable(result) # type: ignore else: return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/wrappers/request.py new/quart-trio-0.13.0/src/quart_trio/wrappers/request.py --- old/quart-trio-0.12.0/src/quart_trio/wrappers/request.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/wrappers/request.py 2026-09-12 18:51:24.000000000 +0200 @@ -4,6 +4,7 @@ from quart.wrappers.request import Body, Request from werkzeug.exceptions import RequestEntityTooLarge, RequestTimeout +from ..datastructures import TrioQueue from ..formparser import TrioFormDataParser @@ -28,9 +29,9 @@ def __init__( self, expected_content_length: Optional[int], max_content_length: Optional[int] ) -> None: - self._data = bytearray() + self._data = None self._complete = EventWrapper() # type: ignore - self._has_data = EventWrapper() # type: ignore + self._queue = TrioQueue[bytes]() # type: ignore self._max_content_length = max_content_length # Exceptions must be raised within application (not ASGI) # calls, this is achieved by having the ASGI methods set this diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/wrappers/response.py new/quart-trio-0.13.0/src/quart_trio/wrappers/response.py --- old/quart-trio-0.12.0/src/quart_trio/wrappers/response.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/wrappers/response.py 2026-09-12 18:51:24.000000000 +0200 @@ -79,7 +79,7 @@ if isasyncgen(iterable): self.iter = iterable elif isgenerator(iterable): - self.iter = run_sync_iterable(iterable) + self.iter = run_sync_iterable(iterable) # type: ignore else: async def _aiter() -> AsyncGenerator[bytes, None]: @@ -91,4 +91,4 @@ class TrioResponse(Response): file_body_class = TrioFileBody # type: ignore - iterable_body_class = TrioIterableBody + iterable_body_class = TrioIterableBody # type: ignore diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/src/quart_trio/wrappers/websocket.py new/quart-trio-0.13.0/src/quart_trio/wrappers/websocket.py --- old/quart-trio-0.12.0/src/quart_trio/wrappers/websocket.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/src/quart_trio/wrappers/websocket.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,9 +1,16 @@ -from typing import AnyStr +from quart.wrappers.websocket import Buffer, Websocket -from quart.wrappers.websocket import Websocket +from ..datastructures import TrioQueue + + +class TrioBuffer(Buffer): + def __init__(self) -> None: + self._queue = TrioQueue[bytes | str]() # type: ignore class TrioWebsocket(Websocket): - async def send(self, data: AnyStr) -> None: + buffer_class = TrioBuffer + + async def send(self, data: str | bytes) -> None: await self.accept() await self._send(data) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/tests/test_asgi.py new/quart-trio-0.13.0/tests/test_asgi.py --- old/quart-trio-0.12.0/tests/test_asgi.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/tests/test_asgi.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,3 +1,5 @@ +from unittest.mock import AsyncMock + import pytest import trio from hypercorn.typing import WebsocketScope @@ -25,10 +27,11 @@ "state": {}, # type: ignore[typeddict-item] } connection = TrioASGIWebsocketConnection(QuartTrio(__name__), scope) + websocket = connection._create_websocket_from_scope(AsyncMock()) send_channel, receive_channel = trio.open_memory_channel[dict](0) async with trio.open_nursery() as nursery: nursery.start_soon( - connection.handle_messages, nursery, receive_channel.receive # type: ignore + connection.handle_messages, nursery, websocket, receive_channel.receive # type: ignore ) await send_channel.send({"type": "websocket.disconnect"}) await trio.sleep(1) # Simulate doing something else diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/tests/test_basic.py new/quart-trio-0.13.0/tests/test_basic.py --- old/quart-trio-0.12.0/tests/test_basic.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/tests/test_basic.py 2026-09-12 18:51:24.000000000 +0200 @@ -48,7 +48,7 @@ async with test_client.websocket("/ws/") as test_websocket: await test_websocket.send(data) result = await test_websocket.receive() - assert result == data # type: ignore + assert result == data @pytest.mark.trio diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/tests/test_request.py new/quart-trio-0.13.0/tests/test_request.py --- old/quart-trio-0.12.0/tests/test_request.py 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/tests/test_request.py 2026-09-12 18:51:24.000000000 +0200 @@ -1,4 +1,6 @@ import pytest +import trio +from quart.wrappers.base import ClientDisconnectedError from werkzeug.exceptions import RequestEntityTooLarge from quart_trio.wrappers.request import TrioBody @@ -8,6 +10,30 @@ async def test_body_exceeds_max_content_length() -> None: max_content_length = 5 body = TrioBody(None, max_content_length) - body.append(b" " * (max_content_length + 1)) + await body.put(b" " * (max_content_length + 1)) + body.set_complete() with pytest.raises(RequestEntityTooLarge): await body + + [email protected] +async def test_body_streaming() -> None: + body = TrioBody(None, None) + + async def produce() -> None: + await body.put(b"first") + await body.put(b"second") + body.set_complete() + + async with trio.open_nursery() as nursery: + nursery.start_soon(produce) + data = b"".join([chunk async for chunk in body]) + assert data == b"firstsecond" + + [email protected] +async def test_body_disconnect() -> None: + body = TrioBody(None, None) + body.disconnect() + with pytest.raises(ClientDisconnectedError): + await body.get() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/quart-trio-0.12.0/tox.ini new/quart-trio-0.13.0/tox.ini --- old/quart-trio-0.12.0/tox.ini 2025-01-09 22:04:35.000000000 +0100 +++ new/quart-trio-0.13.0/tox.ini 2026-09-12 18:51:24.000000000 +0200 @@ -1,5 +1,5 @@ [tox] -envlist = docs,format,mypy,py39,py310,py311,py312,py313,pep8,package +envlist = docs,format,mypy,py313,py314,pep8,package isolated_build = True [testenv] @@ -11,7 +11,7 @@ commands = pytest --cov=quart_trio {posargs} [testenv:docs] -basepython = python3.13 +basepython = python3.14 deps = pydata-sphinx-theme sphinx @@ -20,7 +20,7 @@ sphinx-build -b html -d {envtmpdir}/doctrees docs/ docs/_build/html/ [testenv:format] -basepython = python3.13 +basepython = python3.14 deps = black isort @@ -29,7 +29,7 @@ isort --check --diff src/quart_trio/ tests [testenv:pep8] -basepython = python3.13 +basepython = python3.14 deps = flake8 pep8-naming @@ -37,7 +37,7 @@ commands = flake8 src/quart_trio/ tests/ [testenv:mypy] -basepython = python3.13 +basepython = python3.14 deps = mypy pytest @@ -45,7 +45,7 @@ mypy src/quart_trio/ tests/ [testenv:package] -basepython = python3.13 +basepython = python3.14 deps = pdm twine
