Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/threadlocal.py: 38%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

130 statements  

1# SPDX-License-Identifier: MIT OR Apache-2.0 

2# This file is dual licensed under the terms of the Apache License, Version 

3# 2.0, and the MIT License. See the LICENSE file in the root of this 

4# repository for complete details. 

5 

6""" 

7**Deprecated** primitives to keep context global but thread (and greenlet) 

8local. 

9 

10See `thread-local`, but please use :doc:`contextvars` instead. 

11 

12.. deprecated:: 22.1.0 

13""" 

14 

15from __future__ import annotations 

16 

17import contextlib 

18import sys 

19import threading 

20import uuid 

21import warnings 

22 

23from typing import Any, Generator, Iterator, TypeVar 

24 

25import structlog 

26 

27from ._config import BoundLoggerLazyProxy 

28from .typing import BindableLogger, Context, EventDict, WrappedLogger 

29 

30 

31def _determine_threadlocal() -> type[Any]: 

32 """ 

33 Return a dict-like threadlocal storage depending on whether we run with 

34 greenlets or not. 

35 """ 

36 try: 

37 from ._greenlets import GreenThreadLocal 

38 except ImportError: 

39 from threading import local 

40 

41 return local 

42 

43 return GreenThreadLocal # pragma: no cover 

44 

45 

46ThreadLocal = _determine_threadlocal() 

47 

48 

49def _deprecated() -> None: 

50 """ 

51 Raise a warning with best-effort stacklevel adjustment. 

52 """ 

53 callsite = "" 

54 

55 with contextlib.suppress(Exception): 

56 f = sys._getframe() 

57 callsite = f.f_back.f_back.f_globals[ # type: ignore[union-attr] 

58 "__name__" 

59 ] 

60 

61 # Avoid double warnings if TL functions call themselves. 

62 if callsite == "structlog.threadlocal": 

63 return 

64 

65 stacklevel = 3 

66 # If a function is used as a decorator, we need to add two stack levels. 

67 # This logic will probably break eventually, but it's not worth any more 

68 # complexity. 

69 if callsite == "contextlib": 

70 stacklevel += 2 

71 

72 warnings.warn( 

73 "`structlog.threadlocal` is deprecated, please use " 

74 "`structlog.contextvars` instead.", 

75 DeprecationWarning, 

76 stacklevel=stacklevel, 

77 ) 

78 

79 

80def wrap_dict(dict_class: type[Context]) -> type[Context]: 

81 """ 

82 Wrap a dict-like class and return the resulting class. 

83 

84 The wrapped class and used to keep global in the current thread. 

85 

86 Args: 

87 dict_class: Class used for keeping context. 

88 

89 .. deprecated:: 22.1.0 

90 """ 

91 _deprecated() 

92 Wrapped = type( 

93 "WrappedDict-" + str(uuid.uuid4()), (_ThreadLocalDictWrapper,), {} 

94 ) 

95 Wrapped._tl = ThreadLocal() # type: ignore[attr-defined] 

96 Wrapped._dict_class = dict_class # type: ignore[attr-defined] 

97 

98 return Wrapped 

99 

100 

101TLLogger = TypeVar("TLLogger", bound=BindableLogger) 

102 

103 

104def as_immutable(logger: TLLogger) -> TLLogger: 

105 """ 

106 Extract the context from a thread local logger into an immutable logger. 

107 

108 Args: 

109 logger (structlog.typing.BindableLogger): 

110 A logger with *possibly* thread local state. 

111 

112 Returns: 

113 :class:`~structlog.BoundLogger` with an immutable context. 

114 

115 .. deprecated:: 22.1.0 

116 """ 

117 _deprecated() 

118 if isinstance(logger, BoundLoggerLazyProxy): 

119 logger = logger.bind() 

120 

121 try: 

122 ctx = logger._context._tl.dict_.__class__( # type: ignore[union-attr] 

123 logger._context._dict # type: ignore[union-attr] 

124 ) 

125 bl = logger.__class__( 

126 logger._logger, # type: ignore[attr-defined, call-arg] 

127 processors=logger._processors, # type: ignore[attr-defined] 

128 context={}, 

129 ) 

130 bl._context = ctx 

131 

132 return bl 

133 except AttributeError: 

134 return logger 

135 

136 

137@contextlib.contextmanager 

138def tmp_bind( 

139 logger: TLLogger, **tmp_values: Any 

140) -> Generator[TLLogger, None, None]: 

141 """ 

142 Bind *tmp_values* to *logger* & memorize current state. Rewind afterwards. 

143 

144 Only works with `structlog.threadlocal.wrap_dict`-based contexts. 

145 Use :func:`~structlog.threadlocal.bound_threadlocal` for new code. 

146 

147 .. deprecated:: 22.1.0 

148 """ 

149 _deprecated() 

150 if isinstance(logger, BoundLoggerLazyProxy): 

151 logger = logger.bind() 

152 

153 saved = as_immutable(logger)._context 

154 try: 

155 yield logger.bind(**tmp_values) 

156 finally: 

157 logger._context.clear() 

158 logger._context.update(saved) 

159 

160 

161class _ThreadLocalDictWrapper: 

162 """ 

163 Wrap a dict-like class and keep the state *global* but *thread-local*. 

164 

165 Attempts to re-initialize only updates the wrapped dictionary. 

166 

167 Useful for short-lived threaded applications like requests in web app. 

168 

169 Use :func:`wrap` to instantiate and use 

170 :func:`structlog.BoundLogger.new` to clear the context. 

171 """ 

172 

173 _tl: Any 

174 _dict_class: type[dict[str, Any]] 

175 

176 def __init__(self, *args: Any, **kw: Any) -> None: 

177 """ 

178 We cheat. A context dict gets never recreated. 

179 """ 

180 if args and isinstance(args[0], self.__class__): 

181 # our state is global, no need to look at args[0] if it's of our 

182 # class 

183 self._dict.update(**kw) 

184 else: 

185 self._dict.update(*args, **kw) 

186 

187 @property 

188 def _dict(self) -> Context: 

189 """ 

190 Return or create and return the current context. 

191 """ 

192 try: 

193 return self.__class__._tl.dict_ 

194 except AttributeError: 

195 self.__class__._tl.dict_ = self.__class__._dict_class() 

196 

197 return self.__class__._tl.dict_ 

198 

199 def __repr__(self) -> str: 

200 return f"<{self.__class__.__name__}({self._dict!r})>" 

201 

202 def __eq__(self, other: object) -> bool: 

203 # Same class == same dictionary 

204 return self.__class__ == other.__class__ 

205 

206 def __ne__(self, other: object) -> bool: 

207 return not self.__eq__(other) 

208 

209 # Proxy methods necessary for structlog. 

210 # Dunder methods don't trigger __getattr__ so we need to proxy by hand. 

211 def __iter__(self) -> Iterator[str]: 

212 return self._dict.__iter__() 

213 

214 def __setitem__(self, key: str, value: Any) -> None: 

215 self._dict[key] = value 

216 

217 def __delitem__(self, key: str) -> None: 

218 self._dict.__delitem__(key) 

219 

220 def __len__(self) -> int: 

221 return self._dict.__len__() 

222 

223 def __getattr__(self, name: str) -> Any: 

224 return getattr(self._dict, name) 

225 

226 

227_CONTEXT = threading.local() 

228 

229 

230def get_threadlocal() -> Context: 

231 """ 

232 Return a copy of the current thread-local context. 

233 

234 .. versionadded:: 21.2.0 

235 .. deprecated:: 22.1.0 

236 """ 

237 _deprecated() 

238 return _get_context().copy() 

239 

240 

241def get_merged_threadlocal(bound_logger: BindableLogger) -> Context: 

242 """ 

243 Return a copy of the current thread-local context merged with the context 

244 from *bound_logger*. 

245 

246 .. versionadded:: 21.2.0 

247 .. deprecated:: 22.1.0 

248 """ 

249 _deprecated() 

250 ctx = _get_context().copy() 

251 ctx.update(structlog.get_context(bound_logger)) 

252 

253 return ctx 

254 

255 

256def merge_threadlocal( 

257 logger: WrappedLogger, method_name: str, event_dict: EventDict 

258) -> EventDict: 

259 """ 

260 A processor that merges in a global (thread-local) context. 

261 

262 Use this as your first processor in :func:`structlog.configure` to ensure 

263 thread-local context is included in all log calls. 

264 

265 .. versionadded:: 19.2.0 

266 

267 .. versionchanged:: 20.1.0 

268 This function used to be called ``merge_threadlocal_context`` and that 

269 name is still kept around for backward compatibility. 

270 

271 .. deprecated:: 22.1.0 

272 """ 

273 _deprecated() 

274 context = _get_context().copy() 

275 context.update(event_dict) 

276 

277 return context 

278 

279 

280# Alias that shouldn't be used anymore. 

281merge_threadlocal_context = merge_threadlocal 

282 

283 

284def clear_threadlocal() -> None: 

285 """ 

286 Clear the thread-local context. 

287 

288 The typical use-case for this function is to invoke it early in 

289 request-handling code. 

290 

291 .. versionadded:: 19.2.0 

292 .. deprecated:: 22.1.0 

293 """ 

294 _deprecated() 

295 _CONTEXT.context = {} 

296 

297 

298def bind_threadlocal(**kw: Any) -> None: 

299 """ 

300 Put keys and values into the thread-local context. 

301 

302 Use this instead of :func:`~structlog.BoundLogger.bind` when you want some 

303 context to be global (thread-local). 

304 

305 .. versionadded:: 19.2.0 

306 .. deprecated:: 22.1.0 

307 """ 

308 _deprecated() 

309 _get_context().update(kw) 

310 

311 

312def unbind_threadlocal(*keys: str) -> None: 

313 """ 

314 Tries to remove bound *keys* from threadlocal logging context if present. 

315 

316 .. versionadded:: 20.1.0 

317 .. deprecated:: 22.1.0 

318 """ 

319 _deprecated() 

320 context = _get_context() 

321 for key in keys: 

322 context.pop(key, None) 

323 

324 

325@contextlib.contextmanager 

326def bound_threadlocal(**kw: Any) -> Generator[None, None, None]: 

327 """ 

328 Bind *kw* to the current thread-local context. Unbind or restore *kw* 

329 afterwards. Do **not** affect other keys. 

330 

331 Can be used as a context manager or decorator. 

332 

333 .. versionadded:: 21.4.0 

334 .. deprecated:: 22.1.0 

335 """ 

336 _deprecated() 

337 context = get_threadlocal() 

338 saved = {k: context[k] for k in context.keys() & kw.keys()} 

339 

340 bind_threadlocal(**kw) 

341 try: 

342 yield 

343 finally: 

344 unbind_threadlocal(*kw.keys()) 

345 bind_threadlocal(**saved) 

346 

347 

348def _get_context() -> Context: 

349 try: 

350 return _CONTEXT.context 

351 except AttributeError: 

352 _CONTEXT.context = {} 

353 

354 return _CONTEXT.context