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

Reply via email to