Skip to content

Commit fae2ab1

Browse files
authored
Fixed deletion of Local values affecting other contexts. (#523)
1 parent f43e112 commit fae2ab1

File tree

2 files changed

+30
-15
lines changed

2 files changed

+30
-15
lines changed

asgiref/local.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,37 @@
22
import contextlib
33
import contextvars
44
import threading
5-
from typing import Any, Union
5+
from typing import Any, Dict, Union
66

77

88
class _CVar:
99
"""Storage utility for Local."""
1010

1111
def __init__(self) -> None:
12-
self._data: dict[str, contextvars.ContextVar[Any]] = {}
12+
self._data: "contextvars.ContextVar[Dict[str, Any]]" = contextvars.ContextVar(
13+
"asgiref.local"
14+
)
1315

14-
def __getattr__(self, key: str) -> Any:
16+
def __getattr__(self, key):
17+
storage_object = self._data.get({})
1518
try:
16-
var = self._data[key]
19+
return storage_object[key]
1720
except KeyError:
1821
raise AttributeError(f"{self!r} object has no attribute {key!r}")
1922

20-
try:
21-
return var.get()
22-
except LookupError:
23-
raise AttributeError(f"{self!r} object has no attribute {key!r}")
24-
2523
def __setattr__(self, key: str, value: Any) -> None:
2624
if key == "_data":
2725
return super().__setattr__(key, value)
2826

29-
var = self._data.get(key)
30-
if var is None:
31-
self._data[key] = var = contextvars.ContextVar(key)
32-
var.set(value)
27+
storage_object = self._data.get({}).copy()
28+
storage_object[key] = value
29+
self._data.set(storage_object)
3330

3431
def __delattr__(self, key: str) -> None:
35-
if key in self._data:
36-
del self._data[key]
32+
storage_object = self._data.get({}).copy()
33+
if key in storage_object:
34+
del storage_object[key]
35+
self._data.set(storage_object)
3736
else:
3837
raise AttributeError(f"{self!r} object has no attribute {key!r}")
3938

tests/test_local.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,19 @@ async def _test() -> None:
375375

376376
# Changes should not leak to the caller
377377
assert test_local.value == 0
378+
379+
380+
@pytest.mark.asyncio
381+
async def test_deletion() -> None:
382+
"""Check visibility with asyncio tasks."""
383+
test_local = Local()
384+
test_local.value = 123
385+
386+
async def _test() -> None:
387+
# Local is inherited when changing task
388+
assert test_local.value == 123
389+
del test_local.value
390+
assert not hasattr(test_local, "value")
391+
392+
await asyncio.create_task(_test())
393+
assert test_local.value == 123

0 commit comments

Comments
 (0)