asgiref 3.9.2__tar.gz → 3.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {asgiref-3.9.2/asgiref.egg-info → asgiref-3.11.0}/PKG-INFO +1 -1
  2. asgiref-3.11.0/asgiref/__init__.py +1 -0
  3. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/sync.py +75 -4
  4. {asgiref-3.9.2 → asgiref-3.11.0/asgiref.egg-info}/PKG-INFO +1 -1
  5. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_sync.py +94 -0
  6. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_sync_contextvars.py +65 -1
  7. asgiref-3.9.2/asgiref/__init__.py +0 -1
  8. {asgiref-3.9.2 → asgiref-3.11.0}/LICENSE +0 -0
  9. {asgiref-3.9.2 → asgiref-3.11.0}/MANIFEST.in +0 -0
  10. {asgiref-3.9.2 → asgiref-3.11.0}/README.rst +0 -0
  11. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/compatibility.py +0 -0
  12. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/current_thread_executor.py +0 -0
  13. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/local.py +0 -0
  14. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/py.typed +0 -0
  15. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/server.py +0 -0
  16. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/testing.py +0 -0
  17. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/timeout.py +0 -0
  18. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/typing.py +0 -0
  19. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/wsgi.py +0 -0
  20. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/SOURCES.txt +0 -0
  21. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/dependency_links.txt +0 -0
  22. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/not-zip-safe +0 -0
  23. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/requires.txt +0 -0
  24. {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/top_level.txt +0 -0
  25. {asgiref-3.9.2 → asgiref-3.11.0}/setup.cfg +0 -0
  26. {asgiref-3.9.2 → asgiref-3.11.0}/setup.py +0 -0
  27. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_compatibility.py +0 -0
  28. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_garbage_collection.py +0 -0
  29. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_local.py +0 -0
  30. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_server.py +0 -0
  31. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_testing.py +0 -0
  32. {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_wsgi.py +0 -0
  33. {asgiref-3.9.2 → asgiref-3.11.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.9.2
3
+ Version: 3.11.0
4
4
  Summary: ASGI specs, helper code, and adapters
5
5
  Home-page: https://github.com/django/asgiref/
6
6
  Author: Django Software Foundation
@@ -0,0 +1 @@
1
+ __version__ = "3.11.0"
@@ -69,6 +69,45 @@ else:
69
69
  return func
70
70
 
71
71
 
72
+ class AsyncSingleThreadContext:
73
+ """Context manager to run async code inside the same thread.
74
+
75
+ Normally, AsyncToSync functions run either inside a separate ThreadPoolExecutor or
76
+ the main event loop if it exists. This context manager ensures that all AsyncToSync
77
+ functions execute within the same thread.
78
+
79
+ This context manager is re-entrant, so only the outer-most call to
80
+ AsyncSingleThreadContext will set the context.
81
+
82
+ Usage:
83
+
84
+ >>> import asyncio
85
+ >>> with AsyncSingleThreadContext():
86
+ ... async_to_sync(asyncio.sleep(1))()
87
+ """
88
+
89
+ def __init__(self):
90
+ self.token = None
91
+
92
+ def __enter__(self):
93
+ try:
94
+ AsyncToSync.async_single_thread_context.get()
95
+ except LookupError:
96
+ self.token = AsyncToSync.async_single_thread_context.set(self)
97
+
98
+ return self
99
+
100
+ def __exit__(self, exc, value, tb):
101
+ if not self.token:
102
+ return
103
+
104
+ executor = AsyncToSync.context_to_thread_executor.pop(self, None)
105
+ if executor:
106
+ executor.shutdown()
107
+
108
+ AsyncToSync.async_single_thread_context.reset(self.token)
109
+
110
+
72
111
  class ThreadSensitiveContext:
73
112
  """Async context manager to manage context for thread sensitive mode
74
113
 
@@ -131,6 +170,14 @@ class AsyncToSync(Generic[_P, _R]):
131
170
  # inside create_task, we'll look it up here from the running event loop.
132
171
  loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {}
133
172
 
173
+ async_single_thread_context: "contextvars.ContextVar[AsyncSingleThreadContext]" = (
174
+ contextvars.ContextVar("async_single_thread_context")
175
+ )
176
+
177
+ context_to_thread_executor: "weakref.WeakKeyDictionary[AsyncSingleThreadContext, ThreadPoolExecutor]" = (
178
+ weakref.WeakKeyDictionary()
179
+ )
180
+
134
181
  def __init__(
135
182
  self,
136
183
  awaitable: Union[
@@ -246,8 +293,24 @@ class AsyncToSync(Generic[_P, _R]):
246
293
  running_in_main_event_loop = False
247
294
 
248
295
  if not running_in_main_event_loop:
249
- # Make our own event loop - in a new thread - and run inside that.
250
- loop_executor = ThreadPoolExecutor(max_workers=1)
296
+ loop_executor = None
297
+
298
+ if self.async_single_thread_context.get(None):
299
+ single_thread_context = self.async_single_thread_context.get()
300
+
301
+ if single_thread_context in self.context_to_thread_executor:
302
+ loop_executor = self.context_to_thread_executor[
303
+ single_thread_context
304
+ ]
305
+ else:
306
+ loop_executor = ThreadPoolExecutor(max_workers=1)
307
+ self.context_to_thread_executor[
308
+ single_thread_context
309
+ ] = loop_executor
310
+ else:
311
+ # Make our own event loop - in a new thread - and run inside that.
312
+ loop_executor = ThreadPoolExecutor(max_workers=1)
313
+
251
314
  loop_future = loop_executor.submit(asyncio.run, new_loop_wrap())
252
315
  # Run the CurrentThreadExecutor until the future is done.
253
316
  current_executor.run_until_future(loop_future)
@@ -361,6 +424,7 @@ class SyncToAsync(Generic[_P, _R]):
361
424
  func: Callable[_P, _R],
362
425
  thread_sensitive: bool = True,
363
426
  executor: Optional["ThreadPoolExecutor"] = None,
427
+ context: Optional[contextvars.Context] = None,
364
428
  ) -> None:
365
429
  if (
366
430
  not callable(func)
@@ -369,6 +433,7 @@ class SyncToAsync(Generic[_P, _R]):
369
433
  ):
370
434
  raise TypeError("sync_to_async can only be applied to sync functions.")
371
435
  self.func = func
436
+ self.context = context
372
437
  functools.update_wrapper(self, func)
373
438
  self._thread_sensitive = thread_sensitive
374
439
  markcoroutinefunction(self)
@@ -417,7 +482,7 @@ class SyncToAsync(Generic[_P, _R]):
417
482
  # Use the passed in executor, or the loop's default if it is None
418
483
  executor = self._executor
419
484
 
420
- context = contextvars.copy_context()
485
+ context = contextvars.copy_context() if self.context is None else self.context
421
486
  child = functools.partial(self.func, *args, **kwargs)
422
487
  func = context.run
423
488
  task_context: List[asyncio.Task[Any]] = []
@@ -455,7 +520,8 @@ class SyncToAsync(Generic[_P, _R]):
455
520
  exec_coro.cancel()
456
521
  ret = await exec_coro
457
522
  finally:
458
- _restore_context(context)
523
+ if self.context is None:
524
+ _restore_context(context)
459
525
  self.deadlock_context.set(False)
460
526
 
461
527
  return ret
@@ -548,6 +614,7 @@ def sync_to_async(
548
614
  *,
549
615
  thread_sensitive: bool = True,
550
616
  executor: Optional["ThreadPoolExecutor"] = None,
617
+ context: Optional[contextvars.Context] = None,
551
618
  ) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]:
552
619
  ...
553
620
 
@@ -558,6 +625,7 @@ def sync_to_async(
558
625
  *,
559
626
  thread_sensitive: bool = True,
560
627
  executor: Optional["ThreadPoolExecutor"] = None,
628
+ context: Optional[contextvars.Context] = None,
561
629
  ) -> Callable[_P, Coroutine[Any, Any, _R]]:
562
630
  ...
563
631
 
@@ -567,6 +635,7 @@ def sync_to_async(
567
635
  *,
568
636
  thread_sensitive: bool = True,
569
637
  executor: Optional["ThreadPoolExecutor"] = None,
638
+ context: Optional[contextvars.Context] = None,
570
639
  ) -> Union[
571
640
  Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]],
572
641
  Callable[_P, Coroutine[Any, Any, _R]],
@@ -576,9 +645,11 @@ def sync_to_async(
576
645
  f,
577
646
  thread_sensitive=thread_sensitive,
578
647
  executor=executor,
648
+ context=context,
579
649
  )
580
650
  return SyncToAsync(
581
651
  func,
582
652
  thread_sensitive=thread_sensitive,
583
653
  executor=executor,
654
+ context=context,
584
655
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.9.2
3
+ Version: 3.11.0
4
4
  Summary: ASGI specs, helper code, and adapters
5
5
  Home-page: https://github.com/django/asgiref/
6
6
  Author: Django Software Foundation
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextvars
2
3
  import functools
3
4
  import multiprocessing
4
5
  import sys
@@ -13,6 +14,7 @@ from unittest import TestCase
13
14
  import pytest
14
15
 
15
16
  from asgiref.sync import (
17
+ AsyncSingleThreadContext,
16
18
  ThreadSensitiveContext,
17
19
  async_to_sync,
18
20
  iscoroutinefunction,
@@ -544,6 +546,98 @@ async def test_thread_sensitive_outside_async():
544
546
  assert result_1["thread"] == result_2["thread"]
545
547
 
546
548
 
549
+ def test_async_single_thread_context_matches():
550
+ """
551
+ Tests that functions wrapped with async_to_sync and executed within an
552
+ AsyncSingleThreadContext run on the same thread, even without a main_event_loop.
553
+ """
554
+ result_1 = {}
555
+ result_2 = {}
556
+
557
+ async def store_thread_async(result):
558
+ result["thread"] = threading.current_thread()
559
+
560
+ with AsyncSingleThreadContext():
561
+ async_to_sync(store_thread_async)(result_1)
562
+ async_to_sync(store_thread_async)(result_2)
563
+
564
+ # They should not have run in the main thread, and on the same threads
565
+ assert result_1["thread"] != threading.current_thread()
566
+ assert result_1["thread"] == result_2["thread"]
567
+
568
+
569
+ def test_async_single_thread_nested_context():
570
+ """
571
+ Tests that behavior remains the same when using nested context managers.
572
+ """
573
+ result_1 = {}
574
+ result_2 = {}
575
+
576
+ @async_to_sync
577
+ async def store_thread(result):
578
+ result["thread"] = threading.current_thread()
579
+
580
+ with AsyncSingleThreadContext():
581
+ store_thread(result_1)
582
+
583
+ with AsyncSingleThreadContext():
584
+ store_thread(result_2)
585
+
586
+ # They should not have run in the main thread, and on the same threads
587
+ assert result_1["thread"] != threading.current_thread()
588
+ assert result_1["thread"] == result_2["thread"]
589
+
590
+
591
+ def test_async_single_thread_context_without_async_work():
592
+ """
593
+ Tests everything works correctly without any async_to_sync calls.
594
+ """
595
+ with AsyncSingleThreadContext():
596
+ pass
597
+
598
+
599
+ def test_async_single_thread_context_success_share_context():
600
+ """
601
+ Tests that we share context between different async_to_sync functions.
602
+ """
603
+ connection = contextvars.ContextVar("connection")
604
+ connection.set(0)
605
+
606
+ async def handler():
607
+ connection.set(connection.get(0) + 1)
608
+
609
+ with AsyncSingleThreadContext():
610
+ async_to_sync(handler)()
611
+ async_to_sync(handler)()
612
+
613
+ assert connection.get() == 2
614
+
615
+
616
+ @pytest.mark.asyncio
617
+ async def test_async_single_thread_context_matches_from_async_thread():
618
+ """
619
+ Tests that we use main_event_loop for running async_to_sync functions executed
620
+ within an AsyncSingleThreadContext.
621
+ """
622
+ result_1 = {}
623
+ result_2 = {}
624
+
625
+ @async_to_sync
626
+ async def store_thread_async(result):
627
+ result["thread"] = threading.current_thread()
628
+
629
+ def inner():
630
+ with AsyncSingleThreadContext():
631
+ store_thread_async(result_1)
632
+ store_thread_async(result_2)
633
+
634
+ await sync_to_async(inner)()
635
+
636
+ # They should both have run in the current thread.
637
+ assert result_1["thread"] == threading.current_thread()
638
+ assert result_1["thread"] == result_2["thread"]
639
+
640
+
547
641
  @pytest.mark.asyncio
548
642
  async def test_thread_sensitive_with_context_matches():
549
643
  result_1 = {}
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import contextvars
3
+ import sys
3
4
  import threading
4
5
  import time
5
6
 
@@ -55,13 +56,76 @@ async def test_sync_to_async_contextvars():
55
56
  assert foo.get() == "baz"
56
57
 
57
58
 
59
+ @pytest.mark.asyncio
60
+ async def test_sync_to_async_contextvars_with_custom_context():
61
+ """
62
+ Passing a custom context to `sync_to_async` ensures that changes to context
63
+ variables within the synchronous function are isolated to the provided
64
+ context and do not affect the caller's context. Specifically, verifies that
65
+ modifications to a context variable inside the sync function are reflected
66
+ only in the custom context and not in the outer context.
67
+ """
68
+
69
+ def sync_function():
70
+ time.sleep(1)
71
+ assert foo.get() == "bar"
72
+ foo.set("baz")
73
+ return 42
74
+
75
+ foo.set("bar")
76
+ context = contextvars.copy_context()
77
+
78
+ async_function = sync_to_async(sync_function, context=context)
79
+ assert await async_function() == 42
80
+
81
+ # Current context remains unchanged.
82
+ assert foo.get() == "bar"
83
+
84
+ # Custom context reflects the changes made within the sync function.
85
+ assert context.get(foo) == "baz"
86
+
87
+
88
+ @pytest.mark.asyncio
89
+ @pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11")
90
+ async def test_sync_to_async_contextvars_with_custom_context_and_parallel_tasks():
91
+ """
92
+ Using a custom context with `sync_to_async` and asyncio tasks isolates
93
+ contextvars changes, leaving the original context unchanged and reflecting
94
+ all modifications in the custom context.
95
+ """
96
+ foo.set("")
97
+
98
+ def sync_function():
99
+ foo.set(foo.get() + "1")
100
+ return 1
101
+
102
+ async def async_function():
103
+ foo.set(foo.get() + "1")
104
+ return 1
105
+
106
+ context = contextvars.copy_context()
107
+
108
+ await asyncio.gather(
109
+ sync_to_async(sync_function, context=context)(),
110
+ sync_to_async(sync_function, context=context)(),
111
+ asyncio.create_task(async_function(), context=context),
112
+ asyncio.create_task(async_function(), context=context),
113
+ )
114
+
115
+ # Current context remains unchanged
116
+ assert foo.get() == ""
117
+
118
+ # Custom context reflects the changes made within all the gathered tasks.
119
+ assert context.get(foo) == "1111"
120
+
121
+
58
122
  def test_async_to_sync_contextvars():
59
123
  """
60
124
  Tests to make sure that contextvars from the calling context are
61
125
  present in the called context, and that any changes in the called context
62
126
  are then propagated back to the calling context.
63
127
  """
64
- # Define sync function
128
+ # Define async function
65
129
  async def async_function():
66
130
  await asyncio.sleep(1)
67
131
  assert foo.get() == "bar"
@@ -1 +0,0 @@
1
- __version__ = "3.9.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes