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-12-11 18:32:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asgiref (Old)
 and      /work/SRC/openSUSE:Factory/.python-asgiref.new.1939 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-asgiref"

Thu Dec 11 18:32:00 2025 rev:14 rq:1321928 version:3.11.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asgiref/python-asgiref.changes    
2025-09-30 17:34:35.773620130 +0200
+++ /work/SRC/openSUSE:Factory/.python-asgiref.new.1939/python-asgiref.changes  
2025-12-11 18:32:17.678682791 +0100
@@ -1,0 +2,11 @@
+Wed Dec  3 22:15:38 UTC 2025 - Guang Yee <[email protected]>
+
+- Update to 3.11.0
+  * ``sync_to_async`` gains a ``context`` parameter, similar to those for
+    ``asyncio.create_task``, ``TaskGroup`` &co, that can be used on Python 
3.11+ to
+    control the context used by the underlying task.
+    The parent context is already propagated by default but the additional
+    control is useful if multiple ``sync_to_async`` calls need to share the 
same
+    context, e.g. when used with ``asyncio.gather()``. 
+
+-------------------------------------------------------------------

Old:
----
  asgiref-3.9.2.tar.gz

New:
----
  asgiref-3.11.0.tar.gz

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

Other differences:
------------------
++++++ python-asgiref.spec ++++++
--- /var/tmp/diff_new_pack.hWfUUR/_old  2025-12-11 18:32:18.326710031 +0100
+++ /var/tmp/diff_new_pack.hWfUUR/_new  2025-12-11 18:32:18.326710031 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-asgiref
-Version:        3.9.2
+Version:        3.11.0
 Release:        0
 Summary:        ASGI specs, helper code, and adapters
 License:        BSD-3-Clause

++++++ asgiref-3.9.2.tar.gz -> asgiref-3.11.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.9.2/PKG-INFO new/asgiref-3.11.0/PKG-INFO
--- old/asgiref-3.9.2/PKG-INFO  2025-09-23 17:00:45.972618300 +0200
+++ new/asgiref-3.11.0/PKG-INFO 2025-11-19 16:32:07.114693600 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: asgiref
-Version: 3.9.2
+Version: 3.11.0
 Summary: ASGI specs, helper code, and adapters
 Home-page: https://github.com/django/asgiref/
 Author: Django Software Foundation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.9.2/asgiref/__init__.py 
new/asgiref-3.11.0/asgiref/__init__.py
--- old/asgiref-3.9.2/asgiref/__init__.py       2025-09-23 16:58:52.000000000 
+0200
+++ new/asgiref-3.11.0/asgiref/__init__.py      2025-11-19 16:31:06.000000000 
+0100
@@ -1 +1 @@
-__version__ = "3.9.2"
+__version__ = "3.11.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.9.2/asgiref/sync.py 
new/asgiref-3.11.0/asgiref/sync.py
--- old/asgiref-3.9.2/asgiref/sync.py   2025-07-03 11:25:24.000000000 +0200
+++ new/asgiref-3.11.0/asgiref/sync.py  2025-11-19 16:29:14.000000000 +0100
@@ -69,6 +69,45 @@
         return func
 
 
+class AsyncSingleThreadContext:
+    """Context manager to run async code inside the same thread.
+
+    Normally, AsyncToSync functions run either inside a separate 
ThreadPoolExecutor or
+    the main event loop if it exists. This context manager ensures that all 
AsyncToSync
+    functions execute within the same thread.
+
+    This context manager is re-entrant, so only the outer-most call to
+    AsyncSingleThreadContext will set the context.
+
+    Usage:
+
+    >>> import asyncio
+    >>> with AsyncSingleThreadContext():
+    ...     async_to_sync(asyncio.sleep(1))()
+    """
+
+    def __init__(self):
+        self.token = None
+
+    def __enter__(self):
+        try:
+            AsyncToSync.async_single_thread_context.get()
+        except LookupError:
+            self.token = AsyncToSync.async_single_thread_context.set(self)
+
+        return self
+
+    def __exit__(self, exc, value, tb):
+        if not self.token:
+            return
+
+        executor = AsyncToSync.context_to_thread_executor.pop(self, None)
+        if executor:
+            executor.shutdown()
+
+        AsyncToSync.async_single_thread_context.reset(self.token)
+
+
 class ThreadSensitiveContext:
     """Async context manager to manage context for thread sensitive mode
 
@@ -131,6 +170,14 @@
     # inside create_task, we'll look it up here from the running event loop.
     loop_thread_executors: "Dict[asyncio.AbstractEventLoop, 
CurrentThreadExecutor]" = {}
 
+    async_single_thread_context: 
"contextvars.ContextVar[AsyncSingleThreadContext]" = (
+        contextvars.ContextVar("async_single_thread_context")
+    )
+
+    context_to_thread_executor: 
"weakref.WeakKeyDictionary[AsyncSingleThreadContext, ThreadPoolExecutor]" = (
+        weakref.WeakKeyDictionary()
+    )
+
     def __init__(
         self,
         awaitable: Union[
@@ -246,8 +293,24 @@
                 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_executor = None
+
+                if self.async_single_thread_context.get(None):
+                    single_thread_context = 
self.async_single_thread_context.get()
+
+                    if single_thread_context in 
self.context_to_thread_executor:
+                        loop_executor = self.context_to_thread_executor[
+                            single_thread_context
+                        ]
+                    else:
+                        loop_executor = ThreadPoolExecutor(max_workers=1)
+                        self.context_to_thread_executor[
+                            single_thread_context
+                        ] = loop_executor
+                else:
+                    # Make our own event loop - in a new thread - and run 
inside that.
+                    loop_executor = ThreadPoolExecutor(max_workers=1)
+
                 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)
@@ -361,6 +424,7 @@
         func: Callable[_P, _R],
         thread_sensitive: bool = True,
         executor: Optional["ThreadPoolExecutor"] = None,
+        context: Optional[contextvars.Context] = None,
     ) -> None:
         if (
             not callable(func)
@@ -369,6 +433,7 @@
         ):
             raise TypeError("sync_to_async can only be applied to sync 
functions.")
         self.func = func
+        self.context = context
         functools.update_wrapper(self, func)
         self._thread_sensitive = thread_sensitive
         markcoroutinefunction(self)
@@ -417,7 +482,7 @@
             # Use the passed in executor, or the loop's default if it is None
             executor = self._executor
 
-        context = contextvars.copy_context()
+        context = contextvars.copy_context() if self.context is None else 
self.context
         child = functools.partial(self.func, *args, **kwargs)
         func = context.run
         task_context: List[asyncio.Task[Any]] = []
@@ -455,7 +520,8 @@
                 exec_coro.cancel()
             ret = await exec_coro
         finally:
-            _restore_context(context)
+            if self.context is None:
+                _restore_context(context)
             self.deadlock_context.set(False)
 
         return ret
@@ -548,6 +614,7 @@
     *,
     thread_sensitive: bool = True,
     executor: Optional["ThreadPoolExecutor"] = None,
+    context: Optional[contextvars.Context] = None,
 ) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]:
     ...
 
@@ -558,6 +625,7 @@
     *,
     thread_sensitive: bool = True,
     executor: Optional["ThreadPoolExecutor"] = None,
+    context: Optional[contextvars.Context] = None,
 ) -> Callable[_P, Coroutine[Any, Any, _R]]:
     ...
 
@@ -567,6 +635,7 @@
     *,
     thread_sensitive: bool = True,
     executor: Optional["ThreadPoolExecutor"] = None,
+    context: Optional[contextvars.Context] = None,
 ) -> Union[
     Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]],
     Callable[_P, Coroutine[Any, Any, _R]],
@@ -576,9 +645,11 @@
             f,
             thread_sensitive=thread_sensitive,
             executor=executor,
+            context=context,
         )
     return SyncToAsync(
         func,
         thread_sensitive=thread_sensitive,
         executor=executor,
+        context=context,
     )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.9.2/asgiref.egg-info/PKG-INFO 
new/asgiref-3.11.0/asgiref.egg-info/PKG-INFO
--- old/asgiref-3.9.2/asgiref.egg-info/PKG-INFO 2025-09-23 17:00:45.000000000 
+0200
+++ new/asgiref-3.11.0/asgiref.egg-info/PKG-INFO        2025-11-19 
16:32:07.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: asgiref
-Version: 3.9.2
+Version: 3.11.0
 Summary: ASGI specs, helper code, and adapters
 Home-page: https://github.com/django/asgiref/
 Author: Django Software Foundation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.9.2/tests/test_sync.py 
new/asgiref-3.11.0/tests/test_sync.py
--- old/asgiref-3.9.2/tests/test_sync.py        2025-07-03 11:25:24.000000000 
+0200
+++ new/asgiref-3.11.0/tests/test_sync.py       2025-10-05 11:02:07.000000000 
+0200
@@ -1,4 +1,5 @@
 import asyncio
+import contextvars
 import functools
 import multiprocessing
 import sys
@@ -13,6 +14,7 @@
 import pytest
 
 from asgiref.sync import (
+    AsyncSingleThreadContext,
     ThreadSensitiveContext,
     async_to_sync,
     iscoroutinefunction,
@@ -544,6 +546,98 @@
     assert result_1["thread"] == result_2["thread"]
 
 
+def test_async_single_thread_context_matches():
+    """
+    Tests that functions wrapped with async_to_sync and executed within an
+    AsyncSingleThreadContext run on the same thread, even without a 
main_event_loop.
+    """
+    result_1 = {}
+    result_2 = {}
+
+    async def store_thread_async(result):
+        result["thread"] = threading.current_thread()
+
+    with AsyncSingleThreadContext():
+        async_to_sync(store_thread_async)(result_1)
+        async_to_sync(store_thread_async)(result_2)
+
+    # They should not have run in the main thread, and on the same threads
+    assert result_1["thread"] != threading.current_thread()
+    assert result_1["thread"] == result_2["thread"]
+
+
+def test_async_single_thread_nested_context():
+    """
+    Tests that behavior remains the same when using nested context managers.
+    """
+    result_1 = {}
+    result_2 = {}
+
+    @async_to_sync
+    async def store_thread(result):
+        result["thread"] = threading.current_thread()
+
+    with AsyncSingleThreadContext():
+        store_thread(result_1)
+
+        with AsyncSingleThreadContext():
+            store_thread(result_2)
+
+    # They should not have run in the main thread, and on the same threads
+    assert result_1["thread"] != threading.current_thread()
+    assert result_1["thread"] == result_2["thread"]
+
+
+def test_async_single_thread_context_without_async_work():
+    """
+    Tests everything works correctly without any async_to_sync calls.
+    """
+    with AsyncSingleThreadContext():
+        pass
+
+
+def test_async_single_thread_context_success_share_context():
+    """
+    Tests that we share context between different async_to_sync functions.
+    """
+    connection = contextvars.ContextVar("connection")
+    connection.set(0)
+
+    async def handler():
+        connection.set(connection.get(0) + 1)
+
+    with AsyncSingleThreadContext():
+        async_to_sync(handler)()
+        async_to_sync(handler)()
+
+    assert connection.get() == 2
+
+
[email protected]
+async def test_async_single_thread_context_matches_from_async_thread():
+    """
+    Tests that we use main_event_loop for running async_to_sync functions 
executed
+    within an AsyncSingleThreadContext.
+    """
+    result_1 = {}
+    result_2 = {}
+
+    @async_to_sync
+    async def store_thread_async(result):
+        result["thread"] = threading.current_thread()
+
+    def inner():
+        with AsyncSingleThreadContext():
+            store_thread_async(result_1)
+            store_thread_async(result_2)
+
+    await sync_to_async(inner)()
+
+    # They should both have run in the current thread.
+    assert result_1["thread"] == threading.current_thread()
+    assert result_1["thread"] == result_2["thread"]
+
+
 @pytest.mark.asyncio
 async def test_thread_sensitive_with_context_matches():
     result_1 = {}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asgiref-3.9.2/tests/test_sync_contextvars.py 
new/asgiref-3.11.0/tests/test_sync_contextvars.py
--- old/asgiref-3.9.2/tests/test_sync_contextvars.py    2024-03-14 
15:06:51.000000000 +0100
+++ new/asgiref-3.11.0/tests/test_sync_contextvars.py   2025-11-19 
16:29:14.000000000 +0100
@@ -1,5 +1,6 @@
 import asyncio
 import contextvars
+import sys
 import threading
 import time
 
@@ -55,13 +56,76 @@
     assert foo.get() == "baz"
 
 
[email protected]
+async def test_sync_to_async_contextvars_with_custom_context():
+    """
+    Passing a custom context to `sync_to_async` ensures that changes to context
+    variables within the synchronous function are isolated to the provided
+    context and do not affect the caller's context. Specifically, verifies that
+    modifications to a context variable inside the sync function are reflected
+    only in the custom context and not in the outer context.
+    """
+
+    def sync_function():
+        time.sleep(1)
+        assert foo.get() == "bar"
+        foo.set("baz")
+        return 42
+
+    foo.set("bar")
+    context = contextvars.copy_context()
+
+    async_function = sync_to_async(sync_function, context=context)
+    assert await async_function() == 42
+
+    # Current context remains unchanged.
+    assert foo.get() == "bar"
+
+    # Custom context reflects the changes made within the sync function.
+    assert context.get(foo) == "baz"
+
+
[email protected]
[email protected](sys.version_info < (3, 11), reason="requires python3.11")
+async def 
test_sync_to_async_contextvars_with_custom_context_and_parallel_tasks():
+    """
+    Using a custom context with `sync_to_async` and asyncio tasks isolates
+    contextvars changes, leaving the original context unchanged and reflecting
+    all modifications in the custom context.
+    """
+    foo.set("")
+
+    def sync_function():
+        foo.set(foo.get() + "1")
+        return 1
+
+    async def async_function():
+        foo.set(foo.get() + "1")
+        return 1
+
+    context = contextvars.copy_context()
+
+    await asyncio.gather(
+        sync_to_async(sync_function, context=context)(),
+        sync_to_async(sync_function, context=context)(),
+        asyncio.create_task(async_function(), context=context),
+        asyncio.create_task(async_function(), context=context),
+    )
+
+    # Current context remains unchanged
+    assert foo.get() == ""
+
+    # Custom context reflects the changes made within all the gathered tasks.
+    assert context.get(foo) == "1111"
+
+
 def test_async_to_sync_contextvars():
     """
     Tests to make sure that contextvars from the calling context are
     present in the called context, and that any changes in the called context
     are then propagated back to the calling context.
     """
-    # Define sync function
+    # Define async function
     async def async_function():
         await asyncio.sleep(1)
         assert foo.get() == "bar"

Reply via email to