Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-asgiref for openSUSE:Factory 
checked in at 2025-07-20 15:28:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asgiref (Old)
 and      /work/SRC/openSUSE:Factory/.python-asgiref.new.8875 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-asgiref"

Sun Jul 20 15:28:36 2025 rev:12 rq:1294058 version:3.9.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asgiref/python-asgiref.changes    
2024-04-09 16:46:35.802677633 +0200
+++ /work/SRC/openSUSE:Factory/.python-asgiref.new.8875/python-asgiref.changes  
2025-07-20 15:29:12.892424113 +0200
@@ -1,0 +2,19 @@
+Thu Jul 17 08:40:37 UTC 2025 - Dirk Müller <dmuel...@suse.com>
+
+- update to 3.9.1:
+  * Adds support for Python 3.13.
+  * Drops support for (end-of-life) Python 3.8.
+  * Fixes an error with conflicting kwargs between AsyncToSync
+    and the wrapped function. (#471)
+  * Fixes Local isolation between asyncio Tasks. (#478)
+  * Fixes a reference cycle in Local (#508)
+  * Fixes a deadlock in CurrentThreadExecutor with nested
+    async_to_sync →
+    sync_to_async → async_to_sync → create_task calls. (#494)
+  * The ApplicationCommunicator testing utility will now return the
+    task result if it's already completed on send_input and
+    receive_nothing.  You may need to catch (e.g.) the
+    asyncio.exceptions.CancelledError if sending messages to
+    already finished consumers in your tests. (#505)
+
+-------------------------------------------------------------------

Old:
----
  asgiref-3.8.1.tar.gz

New:
----
  asgiref-3.9.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-asgiref.spec ++++++
--- /var/tmp/diff_new_pack.oRuEm8/_old  2025-07-20 15:29:14.336483870 +0200
+++ /var/tmp/diff_new_pack.oRuEm8/_new  2025-07-20 15:29:14.336483870 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-asgiref
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2025 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-asgiref
-Version:        3.8.1
+Version:        3.9.1
 Release:        0
 Summary:        ASGI specs, helper code, and adapters
 License:        BSD-3-Clause

++++++ asgiref-3.8.1.tar.gz -> asgiref-3.9.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/PKG-INFO new/asgiref-3.9.1/PKG-INFO
--- old/asgiref-3.8.1/PKG-INFO  2024-03-22 15:39:12.874578500 +0100
+++ new/asgiref-3.9.1/PKG-INFO  2025-07-08 11:07:34.391446800 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: asgiref
-Version: 3.8.1
+Version: 3.9.1
 Summary: ASGI specs, helper code, and adapters
 Home-page: https://github.com/django/asgiref/
 Author: Django Software Foundation
@@ -17,19 +17,20 @@
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
 Classifier: Topic :: Internet :: WWW/HTTP
-Requires-Python: >=3.8
+Requires-Python: >=3.9
 License-File: LICENSE
 Requires-Dist: typing_extensions>=4; python_version < "3.11"
 Provides-Extra: tests
 Requires-Dist: pytest; extra == "tests"
 Requires-Dist: pytest-asyncio; extra == "tests"
-Requires-Dist: mypy>=0.800; extra == "tests"
+Requires-Dist: mypy>=1.14.0; extra == "tests"
+Dynamic: license-file
 
 asgiref
 =======
@@ -129,7 +130,7 @@
 Dependencies
 ------------
 
-``asgiref`` requires Python 3.8 or higher.
+``asgiref`` requires Python 3.9 or higher.
 
 
 Contributing
@@ -179,7 +180,7 @@
 
     python -m build
     twine upload dist/*
-    rm -r build/ dist/
+    rm -r asgiref.egg-info dist
 
 
 Implementation Details
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/README.rst new/asgiref-3.9.1/README.rst
--- old/asgiref-3.8.1/README.rst        2024-03-21 14:08:55.000000000 +0100
+++ new/asgiref-3.9.1/README.rst        2024-12-07 09:15:18.000000000 +0100
@@ -96,7 +96,7 @@
 Dependencies
 ------------
 
-``asgiref`` requires Python 3.8 or higher.
+``asgiref`` requires Python 3.9 or higher.
 
 
 Contributing
@@ -146,7 +146,7 @@
 
     python -m build
     twine upload dist/*
-    rm -r build/ dist/
+    rm -r asgiref.egg-info dist
 
 
 Implementation Details
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/__init__.py 
new/asgiref-3.9.1/asgiref/__init__.py
--- old/asgiref-3.8.1/asgiref/__init__.py       2024-03-22 15:26:51.000000000 
+0100
+++ new/asgiref-3.9.1/asgiref/__init__.py       2025-07-08 11:03:55.000000000 
+0200
@@ -1 +1 @@
-__version__ = "3.8.1"
+__version__ = "3.9.1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/current_thread_executor.py 
new/asgiref-3.9.1/asgiref/current_thread_executor.py
--- old/asgiref-3.8.1/asgiref/current_thread_executor.py        2024-03-21 
14:08:55.000000000 +0100
+++ new/asgiref-3.9.1/asgiref/current_thread_executor.py        2025-07-03 
11:26:09.000000000 +0200
@@ -1,8 +1,8 @@
-import queue
 import sys
 import threading
+from collections import deque
 from concurrent.futures import Executor, Future
-from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
+from typing import Any, Callable, TypeVar
 
 if sys.version_info >= (3, 10):
     from typing import ParamSpec
@@ -53,10 +53,12 @@
     the thread they came from.
     """
 
-    def __init__(self) -> None:
+    def __init__(self, old_executor: "CurrentThreadExecutor | None") -> None:
         self._work_thread = threading.current_thread()
-        self._work_queue: queue.Queue[Union[_WorkItem, "Future[Any]"]] = 
queue.Queue()
-        self._broken = False
+        self._work_ready = threading.Condition(threading.Lock())
+        self._work_items = deque[_WorkItem]()  # synchronized by _work_ready
+        self._broken = False  # synchronized by _work_ready
+        self._old_executor = old_executor
 
     def run_until_future(self, future: "Future[Any]") -> None:
         """
@@ -68,24 +70,30 @@
             raise RuntimeError(
                 "You cannot run CurrentThreadExecutor from a different thread"
             )
-        future.add_done_callback(self._work_queue.put)
-        # Keep getting and running work items until we get the future we're 
waiting for
-        # back via the future's done callback.
-        try:
-            while True:
+
+        def done(future: "Future[Any]") -> None:
+            with self._work_ready:
+                self._broken = True
+                self._work_ready.notify()
+
+        future.add_done_callback(done)
+        # Keep getting and running work items until the future we're waiting 
for
+        # is done and the queue is empty.
+        while True:
+            with self._work_ready:
+                while not self._work_items and not self._broken:
+                    self._work_ready.wait()
+                if not self._work_items:
+                    break
                 # Get a work item and run it
-                work_item = self._work_queue.get()
-                if work_item is future:
-                    return
-                assert isinstance(work_item, _WorkItem)
-                work_item.run()
-                del work_item
-        finally:
-            self._broken = True
+                work_item = self._work_items.popleft()
+            work_item.run()
+            del work_item
 
-    def _submit(
+    def submit(
         self,
         fn: Callable[_P, _R],
+        /,
         *args: _P.args,
         **kwargs: _P.kwargs,
     ) -> "Future[_R]":
@@ -94,22 +102,22 @@
             raise RuntimeError(
                 "You cannot submit onto CurrentThreadExecutor from its own 
thread"
             )
-        # Check they're not too late or the executor errored
-        if self._broken:
-            raise RuntimeError("CurrentThreadExecutor already quit or is 
broken")
-        # Add to work queue
         f: "Future[_R]" = Future()
         work_item = _WorkItem(f, fn, *args, **kwargs)
-        self._work_queue.put(work_item)
-        # Return the future
-        return f
 
-    # Python 3.9+ has a new signature for submit with a "/" after `fn`, to 
enforce
-    # it to be a positional argument. If we ignore[override] mypy on 3.9+ will 
be
-    # happy but 3.8 will say that the ignore comment is unused, even when
-    # defining them differently based on sys.version_info.
-    # We should be able to remove this when we drop support for 3.8.
-    if not TYPE_CHECKING:
+        # Walk up the CurrentThreadExecutor stack to find the closest one still
+        # running
+        executor = self
+        while True:
+            with executor._work_ready:
+                if not executor._broken:
+                    # Add to work queue
+                    executor._work_items.append(work_item)
+                    executor._work_ready.notify()
+                    break
+            if executor._old_executor is None:
+                raise RuntimeError("CurrentThreadExecutor already quit or is 
broken")
+            executor = executor._old_executor
 
-        def submit(self, fn, *args, **kwargs):
-            return self._submit(fn, *args, **kwargs)
+        # Return the future
+        return f
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/local.py 
new/asgiref-3.9.1/asgiref/local.py
--- old/asgiref-3.8.1/asgiref/local.py  2024-03-21 14:08:55.000000000 +0100
+++ new/asgiref-3.9.1/asgiref/local.py  2025-07-08 11:03:38.000000000 +0200
@@ -24,12 +24,12 @@
         if key == "_data":
             return super().__setattr__(key, value)
 
-        storage_object = self._data.get({})
+        storage_object = self._data.get({}).copy()
         storage_object[key] = value
         self._data.set(storage_object)
 
     def __delattr__(self, key: str) -> None:
-        storage_object = self._data.get({})
+        storage_object = self._data.get({}).copy()
         if key in storage_object:
             del storage_object[key]
             self._data.set(storage_object)
@@ -82,12 +82,15 @@
     def _lock_storage(self):
         # Thread safe access to storage
         if self._thread_critical:
+            is_async = True
             try:
                 # this is a test for are we in a async or sync
                 # thread - will raise RuntimeError if there is
                 # no current loop
                 asyncio.get_running_loop()
             except RuntimeError:
+                is_async = False
+            if not is_async:
                 # We are in a sync thread, the storage is
                 # just the plain thread local (i.e, "global within
                 # this thread" - it doesn't matter where you are
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/server.py 
new/asgiref-3.9.1/asgiref/server.py
--- old/asgiref-3.8.1/asgiref/server.py 2024-03-14 15:03:34.000000000 +0100
+++ new/asgiref-3.9.1/asgiref/server.py 2025-07-03 11:24:24.000000000 +0200
@@ -57,12 +57,28 @@
         Runs the asyncio event loop with our handler loop.
         """
         event_loop = asyncio.get_event_loop()
-        asyncio.ensure_future(self.application_checker())
         try:
-            event_loop.run_until_complete(self.handle())
+            event_loop.run_until_complete(self.arun())
         except KeyboardInterrupt:
             logger.info("Exiting due to Ctrl-C/interrupt")
 
+    async def arun(self):
+        """
+        Runs the asyncio event loop with our handler loop.
+        """
+
+        class Done(Exception):
+            pass
+
+        async def handle():
+            await self.handle()
+            raise Done
+
+        try:
+            await asyncio.gather(self.application_checker(), handle())
+        except Done:
+            pass
+
     async def handle(self):
         raise NotImplementedError("You must implement handle()")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/sync.py 
new/asgiref-3.9.1/asgiref/sync.py
--- old/asgiref-3.8.1/asgiref/sync.py   2024-03-22 15:15:14.000000000 +0100
+++ new/asgiref-3.9.1/asgiref/sync.py   2025-07-03 11:25:24.000000000 +0200
@@ -179,15 +179,14 @@
 
         # You can't call AsyncToSync from a thread with a running event loop
         try:
-            event_loop = asyncio.get_running_loop()
+            asyncio.get_running_loop()
         except RuntimeError:
             pass
         else:
-            if event_loop.is_running():
-                raise RuntimeError(
-                    "You cannot use AsyncToSync in the same thread as an async 
event loop - "
-                    "just await the async function directly."
-                )
+            raise RuntimeError(
+                "You cannot use AsyncToSync in the same thread as an async 
event loop - "
+                "just await the async function directly."
+            )
 
         # Make a future for the return information
         call_result: "Future[_R]" = Future()
@@ -196,7 +195,7 @@
         # need one for every sync frame, even if there's one above us in the
         # same thread.
         old_executor = getattr(self.executors, "current", None)
-        current_executor = CurrentThreadExecutor()
+        current_executor = CurrentThreadExecutor(old_executor)
         self.executors.current = current_executor
 
         # Wrapping context in list so it can be reassigned from within
@@ -207,7 +206,6 @@
         # an asyncio.CancelledError to.
         task_context = getattr(SyncToAsync.threadlocal, "task_context", None)
 
-        loop = None
         # Use call_soon_threadsafe to schedule a synchronous callback on the
         # main event loop's thread if it's there, otherwise make a new loop
         # in this thread.
@@ -217,35 +215,45 @@
                 sys.exc_info(),
                 task_context,
                 context,
-                *args,
-                **kwargs,
+                # prepare an awaitable which can be passed as is to 
self.main_wrap,
+                # so that `args` and `kwargs` don't need to be
+                # destructured when passed to self.main_wrap
+                # (which is required by `ParamSpec`)
+                # as that may cause overlapping arguments
+                self.awaitable(*args, **kwargs),
             )
 
-            if not (self.main_event_loop and 
self.main_event_loop.is_running()):
-                # Make our own event loop - in a new thread - and run inside 
that.
-                loop = asyncio.new_event_loop()
+            async def new_loop_wrap() -> None:
+                loop = asyncio.get_running_loop()
                 self.loop_thread_executors[loop] = current_executor
+                try:
+                    await awaitable
+                finally:
+                    del self.loop_thread_executors[loop]
+
+            if self.main_event_loop is not None:
+                try:
+                    self.main_event_loop.call_soon_threadsafe(
+                        self.main_event_loop.create_task, awaitable
+                    )
+                except RuntimeError:
+                    running_in_main_event_loop = False
+                else:
+                    running_in_main_event_loop = True
+                    # Run the CurrentThreadExecutor until the future is done.
+                    current_executor.run_until_future(call_result)
+            else:
+                running_in_main_event_loop = False
+
+            if not running_in_main_event_loop:
+                # Make our own event loop - in a new thread - and run inside 
that.
                 loop_executor = ThreadPoolExecutor(max_workers=1)
-                loop_future = loop_executor.submit(
-                    self._run_event_loop, loop, awaitable
-                )
-                if current_executor:
-                    # Run the CurrentThreadExecutor until the future is done
-                    current_executor.run_until_future(loop_future)
+                loop_future = loop_executor.submit(asyncio.run, 
new_loop_wrap())
+                # Run the CurrentThreadExecutor until the future is done.
+                current_executor.run_until_future(loop_future)
                 # Wait for future and/or allow for exception propagation
                 loop_future.result()
-            else:
-                # Call it inside the existing loop
-                self.main_event_loop.call_soon_threadsafe(
-                    self.main_event_loop.create_task, awaitable
-                )
-                if current_executor:
-                    # Run the CurrentThreadExecutor until the future is done
-                    current_executor.run_until_future(call_result)
         finally:
-            # Clean up any executor we were running
-            if loop is not None:
-                del self.loop_thread_executors[loop]
             _restore_context(context[0])
             # Restore old current thread executor state
             self.executors.current = old_executor
@@ -253,42 +261,6 @@
         # Wait for results from the future.
         return call_result.result()
 
-    def _run_event_loop(self, loop, coro):
-        """
-        Runs the given event loop (designed to be called in a thread).
-        """
-        asyncio.set_event_loop(loop)
-        try:
-            loop.run_until_complete(coro)
-        finally:
-            try:
-                # mimic asyncio.run() behavior
-                # cancel unexhausted async generators
-                tasks = asyncio.all_tasks(loop)
-                for task in tasks:
-                    task.cancel()
-
-                async def gather():
-                    await asyncio.gather(*tasks, return_exceptions=True)
-
-                loop.run_until_complete(gather())
-                for task in tasks:
-                    if task.cancelled():
-                        continue
-                    if task.exception() is not None:
-                        loop.call_exception_handler(
-                            {
-                                "message": "unhandled exception during loop 
shutdown",
-                                "exception": task.exception(),
-                                "task": task,
-                            }
-                        )
-                if hasattr(loop, "shutdown_asyncgens"):
-                    loop.run_until_complete(loop.shutdown_asyncgens())
-            finally:
-                loop.close()
-                asyncio.set_event_loop(self.main_event_loop)
-
     def __get__(self, parent: Any, objtype: Any) -> Callable[_P, _R]:
         """
         Include self for methods
@@ -302,8 +274,7 @@
         exc_info: "OptExcInfo",
         task_context: "Optional[List[asyncio.Task[Any]]]",
         context: List[contextvars.Context],
-        *args: _P.args,
-        **kwargs: _P.kwargs,
+        awaitable: Union[Coroutine[Any, Any, _R], Awaitable[_R]],
     ) -> None:
         """
         Wraps the awaitable with something that puts the result into the
@@ -326,9 +297,9 @@
                 try:
                     raise exc_info[1]
                 except BaseException:
-                    result = await self.awaitable(*args, **kwargs)
+                    result = await awaitable
             else:
-                result = await self.awaitable(*args, **kwargs)
+                result = await awaitable
         except BaseException as e:
             call_result.set_exception(e)
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/testing.py 
new/asgiref-3.9.1/asgiref/testing.py
--- old/asgiref-3.8.1/asgiref/testing.py        2024-03-21 14:08:55.000000000 
+0100
+++ new/asgiref-3.9.1/asgiref/testing.py        2025-07-03 11:24:24.000000000 
+0200
@@ -13,18 +13,40 @@
     """
 
     def __init__(self, application, scope):
+        self._future = None
         self.application = guarantee_single_callable(application)
         self.scope = scope
-        self.input_queue = asyncio.Queue()
-        self.output_queue = asyncio.Queue()
-        # Clear context - this ensures that context vars set in the testing 
scope
-        # are not "leaked" into the application which would normally begin with
-        # an empty context. In Python >= 3.11 this could also be written as:
-        # asyncio.create_task(..., context=contextvars.Context())
-        self.future = contextvars.Context().run(
-            asyncio.create_task,
-            self.application(scope, self.input_queue.get, 
self.output_queue.put),
-        )
+        self._input_queue = None
+        self._output_queue = None
+
+    # For Python 3.9 we need to lazily bind the queues, on 3.10+ they bind the
+    # event loop lazily.
+    @property
+    def input_queue(self):
+        if self._input_queue is None:
+            self._input_queue = asyncio.Queue()
+        return self._input_queue
+
+    @property
+    def output_queue(self):
+        if self._output_queue is None:
+            self._output_queue = asyncio.Queue()
+        return self._output_queue
+
+    @property
+    def future(self):
+        if self._future is None:
+            # Clear context - this ensures that context vars set in the 
testing scope
+            # are not "leaked" into the application which would normally begin 
with
+            # an empty context. In Python >= 3.11 this could also be written 
as:
+            # asyncio.create_task(..., context=contextvars.Context())
+            self._future = contextvars.Context().run(
+                asyncio.create_task,
+                self.application(
+                    self.scope, self.input_queue.get, self.output_queue.put
+                ),
+            )
+        return self._future
 
     async def wait(self, timeout=1):
         """
@@ -46,11 +68,15 @@
                     pass
 
     def stop(self, exceptions=True):
-        if not self.future.done():
-            self.future.cancel()
+        future = self._future
+        if future is None:
+            return
+
+        if not future.done():
+            future.cancel()
         elif exceptions:
             # Give a chance to raise any exceptions
-            self.future.result()
+            future.result()
 
     def __del__(self):
         # Clean up on deletion
@@ -64,6 +90,10 @@
         """
         Sends a single message to the application
         """
+        # Make sure there's not an exception to raise from the task
+        if self.future.done():
+            self.future.result()
+
         # Give it the message
         await self.input_queue.put(message)
 
@@ -94,6 +124,10 @@
         """
         Checks that there is no message to receive in the given time.
         """
+        # Make sure there's not an exception to raise from the task
+        if self.future.done():
+            self.future.result()
+
         # `interval` has precedence over `timeout`
         start = time.monotonic()
         while time.monotonic() - start < timeout:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref/typing.py 
new/asgiref-3.9.1/asgiref/typing.py
--- old/asgiref-3.8.1/asgiref/typing.py 2024-03-21 14:08:55.000000000 +0100
+++ new/asgiref-3.9.1/asgiref/typing.py 2024-12-07 09:15:18.000000000 +0100
@@ -189,6 +189,7 @@
 class WebSocketDisconnectEvent(TypedDict):
     type: Literal["websocket.disconnect"]
     code: int
+    reason: Optional[str]
 
 
 class WebSocketCloseEvent(TypedDict):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref.egg-info/PKG-INFO 
new/asgiref-3.9.1/asgiref.egg-info/PKG-INFO
--- old/asgiref-3.8.1/asgiref.egg-info/PKG-INFO 2024-03-22 15:39:12.000000000 
+0100
+++ new/asgiref-3.9.1/asgiref.egg-info/PKG-INFO 2025-07-08 11:07:34.000000000 
+0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: asgiref
-Version: 3.8.1
+Version: 3.9.1
 Summary: ASGI specs, helper code, and adapters
 Home-page: https://github.com/django/asgiref/
 Author: Django Software Foundation
@@ -17,19 +17,20 @@
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
 Classifier: Topic :: Internet :: WWW/HTTP
-Requires-Python: >=3.8
+Requires-Python: >=3.9
 License-File: LICENSE
 Requires-Dist: typing_extensions>=4; python_version < "3.11"
 Provides-Extra: tests
 Requires-Dist: pytest; extra == "tests"
 Requires-Dist: pytest-asyncio; extra == "tests"
-Requires-Dist: mypy>=0.800; extra == "tests"
+Requires-Dist: mypy>=1.14.0; extra == "tests"
+Dynamic: license-file
 
 asgiref
 =======
@@ -129,7 +130,7 @@
 Dependencies
 ------------
 
-``asgiref`` requires Python 3.8 or higher.
+``asgiref`` requires Python 3.9 or higher.
 
 
 Contributing
@@ -179,7 +180,7 @@
 
     python -m build
     twine upload dist/*
-    rm -r build/ dist/
+    rm -r asgiref.egg-info dist
 
 
 Implementation Details
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref.egg-info/SOURCES.txt 
new/asgiref-3.9.1/asgiref.egg-info/SOURCES.txt
--- old/asgiref-3.8.1/asgiref.egg-info/SOURCES.txt      2024-03-22 
15:39:12.000000000 +0100
+++ new/asgiref-3.9.1/asgiref.egg-info/SOURCES.txt      2025-07-08 
11:07:34.000000000 +0200
@@ -22,6 +22,7 @@
 asgiref.egg-info/requires.txt
 asgiref.egg-info/top_level.txt
 tests/test_compatibility.py
+tests/test_garbage_collection.py
 tests/test_local.py
 tests/test_server.py
 tests/test_sync.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/asgiref.egg-info/requires.txt 
new/asgiref-3.9.1/asgiref.egg-info/requires.txt
--- old/asgiref-3.8.1/asgiref.egg-info/requires.txt     2024-03-22 
15:39:12.000000000 +0100
+++ new/asgiref-3.9.1/asgiref.egg-info/requires.txt     2025-07-08 
11:07:34.000000000 +0200
@@ -5,4 +5,4 @@
 [tests]
 pytest
 pytest-asyncio
-mypy>=0.800
+mypy>=1.14.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/setup.cfg new/asgiref-3.9.1/setup.cfg
--- old/asgiref-3.8.1/setup.cfg 2024-03-22 15:39:12.875497800 +0100
+++ new/asgiref-3.9.1/setup.cfg 2025-07-08 11:07:34.392425000 +0200
@@ -16,11 +16,11 @@
        Programming Language :: Python
        Programming Language :: Python :: 3
        Programming Language :: Python :: 3 :: Only
-       Programming Language :: Python :: 3.8
        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
        Topic :: Internet :: WWW/HTTP
 project_urls = 
        Documentation = https://asgi.readthedocs.io/
@@ -28,7 +28,7 @@
        Changelog = https://github.com/django/asgiref/blob/master/CHANGELOG.txt
 
 [options]
-python_requires = >=3.8
+python_requires = >=3.9
 packages = find:
 include_package_data = true
 install_requires = 
@@ -39,11 +39,12 @@
 tests = 
        pytest
        pytest-asyncio
-       mypy>=0.800
+       mypy>=1.14.0
 
 [tool:pytest]
 testpaths = tests
 asyncio_mode = strict
+asyncio_default_fixture_loop_scope = function
 
 [flake8]
 exclude = venv/*,tox/*,specs/*
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/tests/test_garbage_collection.py 
new/asgiref-3.9.1/tests/test_garbage_collection.py
--- old/asgiref-3.8.1/tests/test_garbage_collection.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/asgiref-3.9.1/tests/test_garbage_collection.py  2025-07-08 
09:43:59.000000000 +0200
@@ -0,0 +1,61 @@
+import gc
+import sys
+
+import pytest
+
+from asgiref.local import Local
+
+
+def disable_gc_for_garbage_collection_test() -> None:
+    # Disable automatic garbage collection. To have control over when
+    # garbage collection is performed. This is necessary to ensure that another
+    # that thread doesn't accidentally trigger it by simply executing code.
+    gc.disable()
+
+    # Delete the garbage list(`gc.garbage`) to ensure that other tests don't
+    # interfere with this test.
+    gc.collect()
+
+    # Set the garbage collection debugging flag to store all unreachable
+    # objects in `gc.garbage`. This is necessary to ensure that the
+    # garbage list is empty after execute test code. Otherwise, the test
+    # will always pass. The garbage list isn't automatically populated
+    # because it costs extra CPU cycles
+    gc.set_debug(gc.DEBUG_SAVEALL)
+
+
+def clean_up_after_garbage_collection_test() -> None:
+    # Clean up the garbage collection settings. Re-enable automatic garbage
+    # collection. This step is mandatory to avoid running other tests without
+    # automatic garbage collection.
+    gc.set_debug(0)
+    gc.enable()
+
+
+@pytest.mark.skipif(
+    sys.implementation.name == "pypy", reason="Test relies on CPython GC 
internals"
+)
+def test_thread_critical_Local_remove_all_reference_cycles() -> None:
+    try:
+        # given
+        # Disable automatic garbage collection and set debugging flag.
+        disable_gc_for_garbage_collection_test()
+
+        # when
+        # Create thread critical Local object in sync context.
+        try:
+            getattr(Local(thread_critical=True), "missing")
+        except AttributeError:
+            pass
+        # Enforce garbage collection to populate the garbage list for 
inspection.
+        gc.collect()
+
+        # then
+        # Ensure that the garbage list is empty. The garbage list is only valid
+        # until the next collection cycle so we can only make assertions about 
it
+        # before re-enabling automatic collection.
+        assert gc.garbage == []
+    # Restore garbage collection settings to their original state. This should 
always be run to avoid interfering
+    # with other tests to ensure that code should be executed in the `finally' 
block.
+    finally:
+        clean_up_after_garbage_collection_test()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/tests/test_local.py 
new/asgiref-3.9.1/tests/test_local.py
--- old/asgiref-3.8.1/tests/test_local.py       2024-03-21 14:08:55.000000000 
+0100
+++ new/asgiref-3.9.1/tests/test_local.py       2025-07-08 11:03:38.000000000 
+0200
@@ -1,6 +1,7 @@
 import asyncio
 import gc
 import threading
+from threading import Thread
 
 import pytest
 
@@ -338,3 +339,55 @@
     # inner value was set inside a new async context, meaning that
     # we do not see it, as context vars don't propagate up the stack
     assert not hasattr(test_local_not_tc, "test_value")
+
+
+def test_visibility_thread_asgiref() -> None:
+    """Check visibility with subthreads."""
+    test_local = Local()
+    test_local.value = 0
+
+    def _test() -> None:
+        # Local() is cleared when changing thread
+        assert not hasattr(test_local, "value")
+        setattr(test_local, "value", 1)
+        assert test_local.value == 1
+
+    thread = Thread(target=_test)
+    thread.start()
+    thread.join()
+
+    assert test_local.value == 0
+
+
+@pytest.mark.asyncio
+async def test_visibility_task() -> None:
+    """Check visibility with asyncio tasks."""
+    test_local = Local()
+    test_local.value = 0
+
+    async def _test() -> None:
+        # Local is inherited when changing task
+        assert test_local.value == 0
+        test_local.value = 1
+        assert test_local.value == 1
+
+    await asyncio.create_task(_test())
+
+    # Changes should not leak to the caller
+    assert test_local.value == 0
+
+
+@pytest.mark.asyncio
+async def test_deletion() -> None:
+    """Check visibility with asyncio tasks."""
+    test_local = Local()
+    test_local.value = 123
+
+    async def _test() -> None:
+        # Local is inherited when changing task
+        assert test_local.value == 123
+        del test_local.value
+        assert not hasattr(test_local, "value")
+
+    await asyncio.create_task(_test())
+    assert test_local.value == 123
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/tests/test_server.py 
new/asgiref-3.9.1/tests/test_server.py
--- old/asgiref-3.8.1/tests/test_server.py      2024-03-14 15:03:34.000000000 
+0100
+++ new/asgiref-3.9.1/tests/test_server.py      2025-07-03 11:24:24.000000000 
+0200
@@ -1,8 +1,8 @@
 import asyncio
 import socket as sock
-from functools import partial
 
 import pytest
+import pytest_asyncio
 
 from asgiref.server import StatelessServer
 
@@ -74,8 +74,8 @@
         self._sock.close()
 
 
-@pytest.fixture(scope="function")
-def server():
+@pytest_asyncio.fixture(scope="function")
+async def server():
     async def app(scope, receive, send):
         while True:
             msg = await receive()
@@ -92,25 +92,12 @@
     assert server_addr == expected_address
 
 
-async def server_auto_close(fut, timeout):
-    """Server run based on run_until_complete. It will block forever with 
handle
-    function because it is a while True loop without break.  Use this method 
to close
-    server automatically."""
-    loop = asyncio.get_running_loop()
-    task = asyncio.ensure_future(fut, loop=loop)
-    await asyncio.sleep(timeout)
-    task.cancel()
-
-
-def test_stateless_server(server):
+@pytest.mark.asyncio
+async def test_stateless_server(server):
     """StatelessServer can be instantiated with an ASGI 3 application."""
     """Create a UDP Server can register instance based on name from message of 
client.
     Clients can communicate to other client by name through server"""
 
-    loop = asyncio.new_event_loop()
-    asyncio.set_event_loop(loop)
-    server.handle = partial(server_auto_close, fut=server.handle(), 
timeout=1.0)
-
     client1 = Client(name="client1")
     client2 = Client(name="client2")
 
@@ -124,30 +111,35 @@
         await check_client_msg(client2, server.address, b"Welcome")
         await check_client_msg(client2, server.address, b"Hello")
 
-    task1 = loop.create_task(check_client1_behavior())
-    task2 = loop.create_task(check_client2_behavior())
+    class Done(Exception):
+        pass
 
-    server.run()
+    async def do_test():
+        await asyncio.gather(check_client1_behavior(), 
check_client2_behavior())
+        raise Done
 
-    assert task1.done()
-    assert task2.done()
+    try:
+        await asyncio.gather(server.arun(), do_test())
+    except Done:
+        pass
 
 
-def test_server_delete_instance(server):
+@pytest.mark.asyncio
+async def test_server_delete_instance(server):
     """The max_applications of Server is 10. After 20 times register, 
application number should be 10."""
-    loop = asyncio.new_event_loop()
-    asyncio.set_event_loop(loop)
-    server.handle = partial(server_auto_close, fut=server.handle(), 
timeout=1.0)
-
     client1 = Client(name="client1")
 
+    class Done(Exception):
+        pass
+
     async def client1_multiple_register():
         for i in range(20):
             await client1.register(server.address, name=f"client{i}")
             print(f"client{i}")
             await check_client_msg(client1, server.address, b"Welcome")
+        raise Done
 
-    task = loop.create_task(client1_multiple_register())
-    server.run()
-
-    assert task.done()
+    try:
+        await asyncio.gather(client1_multiple_register(), server.arun())
+    except Done:
+        pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/tests/test_sync.py 
new/asgiref-3.9.1/tests/test_sync.py
--- old/asgiref-3.8.1/tests/test_sync.py        2024-03-21 15:29:47.000000000 
+0100
+++ new/asgiref-3.9.1/tests/test_sync.py        2025-07-03 11:25:24.000000000 
+0200
@@ -7,6 +7,7 @@
 import warnings
 from concurrent.futures import ThreadPoolExecutor
 from functools import wraps
+from typing import Any
 from unittest import TestCase
 
 import pytest
@@ -1174,3 +1175,109 @@
         assert task_complete
 
     assert task_executed
+
+
+def test_async_to_sync_overlapping_kwargs() -> None:
+    """
+    Tests that AsyncToSync correctly passes through kwargs to the wrapped 
function,
+    particularly in the case where the wrapped function uses same names for 
the parameters
+    as the wrapper.
+    """
+
+    @async_to_sync
+    async def test_function(**kwargs: Any) -> None:
+        assert kwargs
+
+    # AsyncToSync.main_wrap has a param named `context`.
+    # So we pass the same argument here to test for the error
+    # "AsyncToSync.main_wrap() got multiple values for argument '<kwarg>'"
+    test_function(context=1)
+
+
+@pytest.mark.asyncio
+async def test_sync_to_async_overlapping_kwargs() -> None:
+    """
+    Tests that SyncToAsync correctly passes through kwargs to the wrapped 
function,
+    particularly in the case where the wrapped function uses same names for 
the parameters
+    as the wrapper.
+    """
+
+    @sync_to_async
+    def test_function(**kwargs: Any) -> None:
+        assert kwargs
+
+    # SyncToAsync.__call__.loop.run_in_executor has a param named 
`task_context`.
+    await test_function(task_context=1)
+
+
+def test_nested_task() -> None:
+    async def inner() -> asyncio.Task[None]:
+        return asyncio.create_task(sync_to_async(print)("inner"))
+
+    async def main() -> None:
+        task = await sync_to_async(async_to_sync(inner))()
+        await task
+
+    async_to_sync(main)()
+
+
+def test_nested_task_later() -> None:
+    def later(fut: asyncio.Future[asyncio.Task[None]]) -> None:
+        task = asyncio.create_task(sync_to_async(print)("later"))
+        fut.set_result(task)
+
+    async def inner() -> asyncio.Future[asyncio.Task[None]]:
+        loop = asyncio.get_running_loop()
+        fut = loop.create_future()
+        loop.call_later(0.1, later, fut)
+        return fut
+
+    async def main() -> None:
+        fut = await sync_to_async(async_to_sync(inner))()
+        task = await fut
+        await task
+
+    async_to_sync(main)()
+
+
+def test_double_nested_task() -> None:
+    async def inner() -> asyncio.Task[None]:
+        return asyncio.create_task(sync_to_async(print)("inner"))
+
+    async def outer() -> asyncio.Task[asyncio.Task[None]]:
+        return asyncio.create_task(sync_to_async(async_to_sync(inner))())
+
+    async def main() -> None:
+        outer_task = await sync_to_async(async_to_sync(outer))()
+        inner_task = await outer_task
+        await inner_task
+
+    async_to_sync(main)()
+
+
+# asyncio.Barrier is new in Python 3.11. Nest definition (rather than using
+# skipIf) to avoid mypy error.
+if sys.version_info >= (3, 11):
+
+    def test_two_nested_tasks_with_asyncio_run() -> None:
+        barrier = asyncio.Barrier(3)
+        event = threading.Event()
+
+        async def inner() -> None:
+            task = asyncio.create_task(sync_to_async(event.wait)())
+            await barrier.wait()
+            await task
+
+        async def outer() -> tuple[asyncio.Task[None], asyncio.Task[None]]:
+            task0 = asyncio.create_task(inner())
+            task1 = asyncio.create_task(inner())
+            await barrier.wait()
+            event.set()
+            return task0, task1
+
+        async def main() -> None:
+            task0, task1 = await sync_to_async(async_to_sync(outer))()
+            await task0
+            await task1
+
+        asyncio.run(main())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/tests/test_testing.py 
new/asgiref-3.9.1/tests/test_testing.py
--- old/asgiref-3.8.1/tests/test_testing.py     2024-03-14 15:06:51.000000000 
+0100
+++ new/asgiref-3.9.1/tests/test_testing.py     2025-07-03 11:24:24.000000000 
+0200
@@ -1,3 +1,5 @@
+import asyncio
+
 import pytest
 
 from asgiref.testing import ApplicationCommunicator
@@ -43,3 +45,46 @@
     await instance.receive_output()
     # Response received completely
     assert await instance.receive_nothing(0.01) is True
+
+
+def test_receive_nothing_lazy_loop():
+    """
+    Tests ApplicationCommunicator.receive_nothing to return the correct value.
+    """
+    # Get an ApplicationCommunicator instance
+    def wsgi_application(environ, start_response):
+        start_response("200 OK", [])
+        yield b"content"
+
+    application = WsgiToAsgi(wsgi_application)
+    instance = ApplicationCommunicator(
+        application,
+        {
+            "type": "http",
+            "http_version": "1.0",
+            "method": "GET",
+            "path": "/foo/",
+            "query_string": b"bar=baz",
+            "headers": [],
+        },
+    )
+
+    async def test():
+        # No event
+        assert await instance.receive_nothing() is True
+
+        # Produce 3 events to receive
+        await instance.send_input({"type": "http.request"})
+        # Start event of the response
+        assert await instance.receive_nothing() is False
+        await instance.receive_output()
+        # First body event of the response announcing further body event
+        assert await instance.receive_nothing() is False
+        await instance.receive_output()
+        # Last body event of the response
+        assert await instance.receive_nothing() is False
+        await instance.receive_output()
+        # Response received completely
+        assert await instance.receive_nothing(0.01) is True
+
+    asyncio.run(test())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.8.1/tox.ini new/asgiref-3.9.1/tox.ini
--- old/asgiref-3.8.1/tox.ini   2024-03-21 14:08:55.000000000 +0100
+++ new/asgiref-3.9.1/tox.ini   2024-12-07 09:15:18.000000000 +0100
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py{38,39,310,311,312}-{test,mypy}
+    py{38,39,310,311,312,313}-{test,mypy}
     qa
 
 [testenv]

Reply via email to