Skip to content

Commit f8dd171

Browse files
author
Elad Namdar
committed
Reimpl contextvars code
The old impl was broken, since python contextvars impl use shallow copy to copy its context, and using a dict as a contextvar type ends up sharing the same dict among different contexts
1 parent e191df7 commit f8dd171

File tree

3 files changed

+38
-25
lines changed

3 files changed

+38
-25
lines changed

CHANGELOG.rst

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Changes:
2727
- ``structlog.threadlocal.wrap_dict()`` now has a correct type annotation.
2828
`#290 <https://github.com/hynek/structlog/pull/290>`_
2929

30+
- Fixed bug with ``structlog.contextvars`` impl
31+
3032

3133
----
3234

src/structlog/contextvars.py

+32-21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
Python 3.7 as :mod:`contextvars`.
88
99
.. versionadded:: 20.1.0
10+
.. versionchanged:: 14.0.0
11+
Reimplement code without dict
1012
1113
See :doc:`contextvars`.
1214
"""
@@ -15,12 +17,11 @@
1517

1618
from typing import Any, Dict
1719

18-
from .types import Context, EventDict, WrappedLogger
20+
from .types import EventDict, WrappedLogger
1921

2022

21-
_CONTEXT: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar(
22-
"structlog_context"
23-
)
23+
STRUCTLOG_KEY_PREFIX = "structlog_"
24+
_CONTEXT_VARS: Dict[str, contextvars.ContextVar[Any]] = {}
2425

2526

2627
def merge_contextvars(
@@ -33,11 +34,15 @@ def merge_contextvars(
3334
context-local context is included in all log calls.
3435
3536
.. versionadded:: 20.1.0
37+
.. versionchanged:: 20.2.0 See toplevel note
3638
"""
37-
ctx = _get_context().copy()
38-
ctx.update(event_dict)
39+
ctx = contextvars.copy_context()
3940

40-
return ctx
41+
for k in ctx:
42+
if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis:
43+
event_dict.setdefault(k.name[len(STRUCTLOG_KEY_PREFIX) :], ctx[k])
44+
45+
return event_dict
4146

4247

4348
def clear_contextvars() -> None:
@@ -48,9 +53,12 @@ def clear_contextvars() -> None:
4853
handling code.
4954
5055
.. versionadded:: 20.1.0
56+
.. versionchanged:: 20.2.0 See toplevel note
5157
"""
52-
ctx = _get_context()
53-
ctx.clear()
58+
ctx = contextvars.copy_context()
59+
for k in ctx:
60+
if k.name.startswith(STRUCTLOG_KEY_PREFIX):
61+
k.set(Ellipsis)
5462

5563

5664
def bind_contextvars(**kw: Any) -> None:
@@ -61,8 +69,17 @@ def bind_contextvars(**kw: Any) -> None:
6169
context to be global (context-local).
6270
6371
.. versionadded:: 20.1.0
72+
.. versionchanged:: 20.2.0 See toplevel note
6473
"""
65-
_get_context().update(kw)
74+
for k, v in kw.items():
75+
structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}"
76+
try:
77+
var = _CONTEXT_VARS[structlog_k]
78+
except KeyError:
79+
var = contextvars.ContextVar(structlog_k, default=Ellipsis)
80+
_CONTEXT_VARS[structlog_k] = var
81+
82+
var.set(v)
6683

6784

6885
def unbind_contextvars(*keys: str) -> None:
@@ -73,15 +90,9 @@ def unbind_contextvars(*keys: str) -> None:
7390
remove keys from a global (context-local) context.
7491
7592
.. versionadded:: 20.1.0
93+
.. versionchanged:: 20.2.0 See toplevel note
7694
"""
77-
ctx = _get_context()
78-
for key in keys:
79-
ctx.pop(key, None)
80-
81-
82-
def _get_context() -> Context:
83-
try:
84-
return _CONTEXT.get()
85-
except LookupError:
86-
_CONTEXT.set({})
87-
return _CONTEXT.get()
95+
for k in keys:
96+
structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}"
97+
if structlog_k in _CONTEXT_VARS:
98+
_CONTEXT_VARS[structlog_k].set(Ellipsis)

src/structlog/stdlib.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535

3636
try:
37-
from . import contextvars
37+
import contextvars
3838
except ImportError:
3939
contextvars = None # type: ignore
4040

@@ -380,6 +380,7 @@ class AsyncBoundLogger:
380380
It is useful to be able to log synchronously occasionally.
381381
382382
.. versionadded:: 20.2.0
383+
.. versionchanged:: 20.2.0 fix _dispatch_to_sync contextvars usage
383384
"""
384385

385386
__slots__ = ["sync_bl", "_loop"]
@@ -474,11 +475,10 @@ async def _dispatch_to_sync(
474475
"""
475476
Merge contextvars and log using the sync logger in a thread pool.
476477
"""
477-
ctx = contextvars._get_context().copy()
478-
ctx.update(kw)
478+
ctx = contextvars.copy_context()
479479

480480
await self._loop.run_in_executor(
481-
self._executor, partial(meth, event, *args, **ctx)
481+
self._executor, partial(ctx.run, partial(meth, event, *args, **kw))
482482
)
483483

484484
async def debug(self, event: str, *args: Any, **kw: Any) -> None:

0 commit comments

Comments
 (0)