|
1 | 1 | import asyncio
|
| 2 | +from contextvars import copy_context |
2 | 3 |
|
3 | 4 | from jupyter_server import CallContext
|
4 | 5 | from jupyter_server.auth.utils import get_anonymous_username
|
@@ -107,3 +108,39 @@ async def context2():
|
107 | 108 | # Assert that THIS context doesn't have any variables defined.
|
108 | 109 | names = CallContext.context_variable_names()
|
109 | 110 | assert len(names) == 0
|
| 111 | + |
| 112 | + |
| 113 | +async def test_callcontext_with_copy_context_run(): |
| 114 | + """ |
| 115 | + Test scenario: |
| 116 | + - The upper layer uses copy_context().run() |
| 117 | + - Multiple contexts concurrently modify CallContext |
| 118 | + - Verify that no context pollution occurs |
| 119 | + """ |
| 120 | + |
| 121 | + async def context_task(name, value, delay): |
| 122 | + """Coroutine task that modifies CallContext and validates its own values""" |
| 123 | + await asyncio.sleep(delay) |
| 124 | + CallContext.set(name, value) |
| 125 | + # Sleep again to simulate interleaving execution |
| 126 | + await asyncio.sleep(0.1) |
| 127 | + assert CallContext.get(name) == value, f"{name} was polluted" |
| 128 | + # Ensure only the variable written by this context exists |
| 129 | + keys = CallContext.context_variable_names() |
| 130 | + assert name in keys |
| 131 | + assert len(keys) == 1 |
| 132 | + |
| 133 | + # Initialize a variable in the main context |
| 134 | + CallContext.set("foo", "bar3") |
| 135 | + |
| 136 | + # Create two independent copy_context instances |
| 137 | + ctx1 = copy_context() |
| 138 | + ctx2 = copy_context() |
| 139 | + |
| 140 | + # Run coroutines in their respective contexts |
| 141 | + fut1 = asyncio.create_task(ctx1.run(lambda: context_task("foo", "bar1", 0.0))) |
| 142 | + fut2 = asyncio.create_task(ctx2.run(lambda: context_task("foo", "bar2", 0.05))) |
| 143 | + await asyncio.gather(fut1, fut2) |
| 144 | + |
| 145 | + # The main context should remain unaffected (still is empty) |
| 146 | + assert CallContext.get("foo") == "bar3" |
0 commit comments