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

131 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 collections.abc import Generator, Iterator 

24from typing import Any, TypeVar 

25 

26import structlog 

27 

28from ._config import BoundLoggerLazyProxy 

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

30 

31 

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

33 """ 

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

35 greenlets or not. 

36 """ 

37 try: 

38 from ._greenlets import GreenThreadLocal 

39 except ImportError: 

40 from threading import local 

41 

42 return local 

43 

44 return GreenThreadLocal # pragma: no cover 

45 

46 

47ThreadLocal = _determine_threadlocal() 

48 

49 

50def _deprecated() -> None: 

51 """ 

52 Raise a warning with best-effort stacklevel adjustment. 

53 """ 

54 callsite = "" 

55 

56 with contextlib.suppress(Exception): 

57 f = sys._getframe() 

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

59 "__name__" 

60 ] 

61 

62 # Avoid double warnings if TL functions call themselves. 

63 if callsite == "structlog.threadlocal": 

64 return 

65 

66 stacklevel = 3 

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

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

69 # complexity. 

70 if callsite == "contextlib": 

71 stacklevel += 2 

72 

73 warnings.warn( 

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

75 "`structlog.contextvars` instead.", 

76 DeprecationWarning, 

77 stacklevel=stacklevel, 

78 ) 

79 

80 

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

82 """ 

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

84 

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

86 

87 Args: 

88 dict_class: Class used for keeping context. 

89 

90 .. deprecated:: 22.1.0 

91 """ 

92 _deprecated() 

93 Wrapped = type( 

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

95 ) 

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

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

98 

99 return Wrapped 

100 

101 

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

103 

104 

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

106 """ 

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

108 

109 Args: 

110 logger (structlog.typing.BindableLogger): 

111 A logger with *possibly* thread local state. 

112 

113 Returns: 

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

115 

116 .. deprecated:: 22.1.0 

117 """ 

118 _deprecated() 

119 if isinstance(logger, BoundLoggerLazyProxy): 

120 logger = logger.bind() 

121 

122 try: 

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

124 logger._context._dict # type: ignore[attr-defined] 

125 ) 

126 bl = logger.__class__( 

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

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

129 context={}, 

130 ) 

131 bl._context = ctx # type: ignore[misc] 

132 

133 return bl 

134 except AttributeError: 

135 return logger 

136 

137 

138@contextlib.contextmanager 

139def tmp_bind( 

140 logger: TLLogger, **tmp_values: Any 

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

142 """ 

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

144 

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

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

147 

148 .. deprecated:: 22.1.0 

149 """ 

150 _deprecated() 

151 if isinstance(logger, BoundLoggerLazyProxy): 

152 logger = logger.bind() 

153 

154 saved = as_immutable(logger)._context 

155 try: 

156 yield logger.bind(**tmp_values) 

157 finally: 

158 logger._context.clear() 

159 logger._context.update(saved) 

160 

161 

162class _ThreadLocalDictWrapper: 

163 """ 

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

165 

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

167 

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

169 

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

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

172 """ 

173 

174 _tl: Any 

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

176 

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

178 """ 

179 We cheat. A context dict gets never recreated. 

180 """ 

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

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

183 # class 

184 self._dict.update(**kw) 

185 else: 

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

187 

188 @property 

189 def _dict(self) -> Context: 

190 """ 

191 Return or create and return the current context. 

192 """ 

193 try: 

194 return self.__class__._tl.dict_ 

195 except AttributeError: 

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

197 

198 return self.__class__._tl.dict_ 

199 

200 def __repr__(self) -> str: 

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

202 

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

204 # Same class == same dictionary 

205 return self.__class__ == other.__class__ 

206 

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

208 return not self.__eq__(other) 

209 

210 # Proxy methods necessary for structlog. 

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

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

213 return self._dict.__iter__() 

214 

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

216 self._dict[key] = value 

217 

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

219 self._dict.__delitem__(key) 

220 

221 def __len__(self) -> int: 

222 return self._dict.__len__() 

223 

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

225 return getattr(self._dict, name) 

226 

227 

228_CONTEXT = threading.local() 

229 

230 

231def get_threadlocal() -> Context: 

232 """ 

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

234 

235 .. versionadded:: 21.2.0 

236 .. deprecated:: 22.1.0 

237 """ 

238 _deprecated() 

239 return _get_context().copy() 

240 

241 

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

243 """ 

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

245 from *bound_logger*. 

246 

247 .. versionadded:: 21.2.0 

248 .. deprecated:: 22.1.0 

249 """ 

250 _deprecated() 

251 ctx = _get_context().copy() 

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

253 

254 return ctx 

255 

256 

257def merge_threadlocal( 

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

259) -> EventDict: 

260 """ 

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

262 

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

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

265 

266 .. versionadded:: 19.2.0 

267 

268 .. versionchanged:: 20.1.0 

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

270 name is still kept around for backward compatibility. 

271 

272 .. deprecated:: 22.1.0 

273 """ 

274 _deprecated() 

275 context = _get_context().copy() 

276 context.update(event_dict) 

277 

278 return context 

279 

280 

281# Alias that shouldn't be used anymore. 

282merge_threadlocal_context = merge_threadlocal 

283 

284 

285def clear_threadlocal() -> None: 

286 """ 

287 Clear the thread-local context. 

288 

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

290 request-handling code. 

291 

292 .. versionadded:: 19.2.0 

293 .. deprecated:: 22.1.0 

294 """ 

295 _deprecated() 

296 _CONTEXT.context = {} 

297 

298 

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

300 """ 

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

302 

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

304 context to be global (thread-local). 

305 

306 .. versionadded:: 19.2.0 

307 .. deprecated:: 22.1.0 

308 """ 

309 _deprecated() 

310 _get_context().update(kw) 

311 

312 

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

314 """ 

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

316 

317 .. versionadded:: 20.1.0 

318 .. deprecated:: 22.1.0 

319 """ 

320 _deprecated() 

321 context = _get_context() 

322 for key in keys: 

323 context.pop(key, None) 

324 

325 

326@contextlib.contextmanager 

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

328 """ 

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

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

331 

332 Can be used as a context manager or decorator. 

333 

334 .. versionadded:: 21.4.0 

335 .. deprecated:: 22.1.0 

336 """ 

337 _deprecated() 

338 context = get_threadlocal() 

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

340 

341 bind_threadlocal(**kw) 

342 try: 

343 yield 

344 finally: 

345 unbind_threadlocal(*kw.keys()) 

346 bind_threadlocal(**saved) 

347 

348 

349def _get_context() -> Context: 

350 try: 

351 return _CONTEXT.context 

352 except AttributeError: 

353 _CONTEXT.context = {} 

354 

355 return _CONTEXT.context