Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wrapt/synchronization.py: 23%

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

213 statements  

1"""Synchronization decorators and calling-convention markers/bridges. 

2 

3Provides ``synchronized`` for thread and async locking, ``mark_as_sync`` 

4and ``mark_as_async`` for declaring the effective calling convention of a 

5wrapped callable (without converting it), and ``async_to_sync`` / 

6``sync_to_async`` for bridging between the two. 

7""" 

8 

9import asyncio 

10import sys 

11import warnings 

12from functools import partial 

13from inspect import ( 

14 CO_ASYNC_GENERATOR, 

15 CO_COROUTINE, 

16 CO_GENERATOR, 

17 CO_ITERABLE_COROUTINE, 

18 isasyncgenfunction, 

19 iscoroutinefunction, 

20 isgeneratorfunction, 

21) 

22from threading import Lock, RLock 

23 

24from .__wrapt__ import BoundFunctionWrapper, CallableObjectProxy, FunctionWrapper 

25from .decorators import decorator 

26 

27# Calling-convention marker wrappers. These manipulate __code__.co_flags 

28# so that inspect.iscoroutinefunction() reports the intended calling 

29# convention, which lets stdlib code and the synchronized() decorator 

30# auto-select the correct sync or async wrapping behaviour even when 

31# stacked decorators change the effective convention (for example an 

32# inner decorator that invokes an async def via asyncio.run()). 

33 

34 

35class _SyncCodeProxy(CallableObjectProxy): 

36 

37 def __init__(self, wrapped, generator=None): 

38 super().__init__(wrapped) 

39 self._self_generator = generator 

40 

41 @property 

42 def co_flags(self): 

43 original = self.__wrapped__.co_flags 

44 # Strip async-axis and iterable-coroutine bits; sync means neither 

45 # coroutine function nor async generator nor types.coroutine-style. 

46 flags = original & ~(CO_COROUTINE | CO_ASYNC_GENERATOR | CO_ITERABLE_COROUTINE) 

47 if self._self_generator is True: 

48 flags |= CO_GENERATOR 

49 elif self._self_generator is False: 

50 flags &= ~CO_GENERATOR 

51 else: 

52 # Auto: if input was an async generator, preserve generator-ness 

53 # on the sync side by setting CO_GENERATOR. Otherwise leave 

54 # CO_GENERATOR as-is (already copied from the wrapped flags). 

55 if original & CO_ASYNC_GENERATOR: 

56 flags |= CO_GENERATOR 

57 return flags 

58 

59 

60class _SyncFunctionSurrogate(CallableObjectProxy): 

61 

62 def __init__(self, wrapped, generator=None): 

63 super().__init__(wrapped) 

64 self._self_generator = generator 

65 

66 @property 

67 def __code__(self): 

68 return _SyncCodeProxy(self.__wrapped__.__code__, self._self_generator) 

69 

70 

71class _BoundSyncFunctionWrapper(BoundFunctionWrapper): 

72 

73 def __init__(self, *args, **kwargs): 

74 super().__init__(*args, **kwargs) 

75 self._self_is_not_coroutine = True 

76 

77 @property 

78 def __func__(self): 

79 return _SyncFunctionSurrogate( 

80 self.__wrapped__.__func__, self._self_parent._self_generator 

81 ) 

82 

83 

84class _SyncFunctionWrapper(FunctionWrapper): 

85 

86 __bound_function_wrapper__ = _BoundSyncFunctionWrapper 

87 

88 def __init__(self, wrapped, wrapper, generator=None): 

89 super().__init__(wrapped, wrapper) 

90 self._self_is_not_coroutine = True 

91 self._self_generator = generator 

92 

93 @property 

94 def __code__(self): 

95 return _SyncCodeProxy(self.__wrapped__.__code__, self._self_generator) 

96 

97 

98class _AsyncCodeProxy(CallableObjectProxy): 

99 

100 def __init__(self, wrapped, generator=None): 

101 super().__init__(wrapped) 

102 self._self_generator = generator 

103 

104 @property 

105 def co_flags(self): 

106 original = self.__wrapped__.co_flags 

107 # Strip all four convention bits; we reassert the right ones below. 

108 flags = original & ~( 

109 CO_GENERATOR | CO_COROUTINE | CO_ITERABLE_COROUTINE | CO_ASYNC_GENERATOR 

110 ) 

111 if self._self_generator is True: 

112 flags |= CO_ASYNC_GENERATOR 

113 elif self._self_generator is False: 

114 flags |= CO_COROUTINE 

115 else: 

116 # Auto: if input was a generator (sync or async), produce an 

117 # async generator; otherwise produce a coroutine function. 

118 if original & (CO_GENERATOR | CO_ASYNC_GENERATOR): 

119 flags |= CO_ASYNC_GENERATOR 

120 else: 

121 flags |= CO_COROUTINE 

122 return flags 

123 

124 

125class _AsyncFunctionSurrogate(CallableObjectProxy): 

126 

127 def __init__(self, wrapped, generator=None): 

128 super().__init__(wrapped) 

129 self._self_generator = generator 

130 

131 @property 

132 def __code__(self): 

133 return _AsyncCodeProxy(self.__wrapped__.__code__, self._self_generator) 

134 

135 

136class _BoundAsyncFunctionWrapper(BoundFunctionWrapper): 

137 

138 @property 

139 def __func__(self): 

140 return _AsyncFunctionSurrogate( 

141 self.__wrapped__.__func__, self._self_parent._self_generator 

142 ) 

143 

144 

145class _AsyncFunctionWrapper(FunctionWrapper): 

146 

147 __bound_function_wrapper__ = _BoundAsyncFunctionWrapper 

148 

149 def __init__(self, wrapped, wrapper, generator=None): 

150 super().__init__(wrapped, wrapper) 

151 self._self_generator = generator 

152 

153 @property 

154 def __code__(self): 

155 return _AsyncCodeProxy(self.__wrapped__.__code__, self._self_generator) 

156 

157 

158def mark_as_sync(wrapped=None, /, *, generator=None): 

159 """Mark a callable as synchronous from the perspective of calling 

160 convention detection. The returned wrapper is a pass-through that 

161 reports `inspect.iscoroutinefunction()` as False regardless of 

162 whether the underlying callable is declared `async def`. Useful 

163 when a stacked decorator has already collapsed an async function 

164 into a synchronous one (for example by using `asyncio.run()`). 

165 

166 The `generator` keyword toggles the sync generator bit 

167 (`CO_GENERATOR`) on the resulting wrapper. Tri-state: 

168 

169 - `None` (default): auto. Preserve generator-ness from the input -- 

170 if the input was an async generator, the wrapper reports as a sync 

171 generator; otherwise CO_GENERATOR is copied through unchanged. 

172 - `True`: force CO_GENERATOR on. Wrapper reports as a sync generator. 

173 - `False`: force CO_GENERATOR off. Wrapper reports as a plain sync 

174 function even if the input had CO_GENERATOR set. 

175 

176 Regardless of `generator`, CO_COROUTINE, CO_ASYNC_GENERATOR, and 

177 CO_ITERABLE_COROUTINE are all cleared (sync means none of those). 

178 """ 

179 

180 def _decorator(wrapped): 

181 def _wrapper(wrapped, instance, args, kwargs): 

182 return wrapped(*args, **kwargs) 

183 

184 return _SyncFunctionWrapper(wrapped, _wrapper, generator=generator) 

185 

186 if wrapped is None: 

187 return _decorator 

188 return _decorator(wrapped) 

189 

190 

191def mark_as_async(wrapped=None, /, *, generator=None): 

192 """Mark a callable as asynchronous from the perspective of calling 

193 convention detection. The returned wrapper reports 

194 `inspect.iscoroutinefunction()` as True regardless of whether the 

195 underlying callable is declared `async def`. Useful when a stacked 

196 decorator returns a coroutine from a plain `def` wrapper. 

197 

198 The `generator` keyword chooses between coroutine function and 

199 async generator reporting. Tri-state: 

200 

201 - `None` (default): auto. If the input was a sync or async 

202 generator, the wrapper reports as an async generator 

203 (`CO_ASYNC_GENERATOR`); otherwise it reports as a coroutine 

204 function (`CO_COROUTINE`). 

205 - `True`: force async generator reporting (`CO_ASYNC_GENERATOR` set, 

206 `CO_COROUTINE` cleared). These two flags are mutually exclusive at 

207 the CPython code-object level. 

208 - `False`: force coroutine function reporting (`CO_COROUTINE` set, 

209 `CO_ASYNC_GENERATOR` cleared). 

210 

211 CO_GENERATOR and CO_ITERABLE_COROUTINE are always cleared (the 

212 async path does not use either). 

213 """ 

214 

215 def _decorator(wrapped): 

216 def _wrapper(wrapped, instance, args, kwargs): 

217 return wrapped(*args, **kwargs) 

218 

219 return _AsyncFunctionWrapper(wrapped, _wrapper, generator=generator) 

220 

221 if wrapped is None: 

222 return _decorator 

223 return _decorator(wrapped) 

224 

225 

226def async_to_sync(wrapped): 

227 """Adapt an async callable so it can be called synchronously. Each 

228 call runs the coroutine to completion via `asyncio.run()`. The 

229 returned wrapper reports as synchronous under 

230 `inspect.iscoroutinefunction()`. Naming follows the asgiref 

231 convention. 

232 

233 Generator functions of either convention are rejected with a 

234 `TypeError`: `asyncio.run()` cannot run an async generator, and a 

235 sync generator has nothing to run to completion. A callable not 

236 reporting as a coroutine function produces a `UserWarning` rather 

237 than an error, since convention detection has false negatives such 

238 as a plain function wrapper which returns a coroutine; if the 

239 callable really is asynchronous in behaviour, apply 

240 `mark_as_async` to it first so it reports as such.""" 

241 

242 if isasyncgenfunction(wrapped): 

243 raise TypeError( 

244 "async_to_sync cannot be applied to an async generator " 

245 "function; asyncio.run() cannot run an async generator." 

246 ) 

247 

248 if isgeneratorfunction(wrapped): 

249 raise TypeError( 

250 "async_to_sync cannot be applied to a generator function; " 

251 "there is no coroutine to run to completion. If the callable " 

252 "is misreported and actually asynchronous in behaviour, apply " 

253 "mark_as_async to it first." 

254 ) 

255 

256 if not iscoroutinefunction(wrapped): 

257 # Detection has false negatives, such as a plain def wrapper 

258 # which returns a coroutine, so this cannot be an error. Such a 

259 # callable works here regardless, but marking it first also 

260 # corrects what introspection reports elsewhere. 

261 warnings.warn( 

262 "async_to_sync was applied to a callable not reporting as a " 

263 "coroutine function. If it is asynchronous in behaviour, " 

264 "apply mark_as_async to it first so it reports as such.", 

265 UserWarning, 

266 stacklevel=2, 

267 ) 

268 

269 def wrapper(wrapped, instance, args, kwargs): 

270 return asyncio.run(wrapped(*args, **kwargs)) 

271 

272 return _SyncFunctionWrapper(wrapped, wrapper, generator=False) 

273 

274 

275def sync_to_async(wrapped): 

276 """Adapt a sync callable so it can be awaited. Each call dispatches 

277 the synchronous work to the default executor via 

278 `loop.run_in_executor()`. The returned wrapper reports as 

279 asynchronous under `inspect.iscoroutinefunction()`. Naming follows 

280 the asgiref convention. 

281 

282 Callables reporting as coroutine functions or generator functions 

283 of either convention are rejected with a `TypeError`. For a 

284 generator function the adaptation would be an illusion: only 

285 creation of the generator would be dispatched to the executor, 

286 which executes no body code, while each iteration would still block 

287 the event loop. A callable misreported as asynchronous but 

288 synchronous in behaviour can have `mark_as_sync` applied to it 

289 first so it reports as such.""" 

290 

291 if iscoroutinefunction(wrapped) or isasyncgenfunction(wrapped): 

292 # Mirrors the equivalent check in asgiref. A callable which is 

293 # misreported, such as a wrapper which runs an inner async def 

294 # to completion, can be corrected with mark_as_sync first. 

295 raise TypeError( 

296 "sync_to_async can only be applied to a synchronous " 

297 "callable. If the callable is misreported and actually " 

298 "synchronous in behaviour, apply mark_as_sync to it first." 

299 ) 

300 

301 if isgeneratorfunction(wrapped): 

302 raise TypeError( 

303 "sync_to_async cannot be applied to a generator function; " 

304 "creating the generator executes no code, and iterating it " 

305 "would still block the event loop." 

306 ) 

307 

308 async def wrapper(wrapped, instance, args, kwargs): 

309 loop = asyncio.get_running_loop() 

310 return await loop.run_in_executor(None, partial(wrapped, *args, **kwargs)) 

311 

312 return _AsyncFunctionWrapper(wrapped, wrapper, generator=False) 

313 

314 

315def _synchronized_is_async_lock(obj): 

316 return iscoroutinefunction(getattr(obj, "acquire", None)) 

317 

318 

319def _synchronized_is_async_callable(obj): 

320 # Walk the __wrapped__ chain, returning True as soon as any layer 

321 # declares itself a coroutine function. A sync marker wrapper can 

322 # carry an authoritative `_self_is_not_coroutine` attribute that 

323 # short-circuits the walk before it descends into a genuinely 

324 # async inner layer. Cycle / runaway-chain protection modelled on 

325 # inspect.unwrap(). 

326 

327 memo = {id(obj): obj} 

328 recursion_limit = sys.getrecursionlimit() 

329 target = obj 

330 

331 while True: 

332 if isinstance(target, (classmethod, staticmethod)): 

333 inner = getattr(target, "__wrapped__", None) 

334 if inner is None: 

335 inner = target.__func__ 

336 target = inner 

337 id_target = id(target) 

338 if id_target in memo or len(memo) >= recursion_limit: 

339 raise ValueError("wrapper loop when unwrapping {!r}".format(obj)) 

340 memo[id_target] = target 

341 continue 

342 

343 if getattr(target, "_self_is_not_coroutine", False): 

344 return False 

345 

346 if iscoroutinefunction(target): 

347 return True 

348 

349 next_target = getattr(target, "__wrapped__", None) 

350 if next_target is None or next_target is target: 

351 return False 

352 target = next_target 

353 id_target = id(target) 

354 if id_target in memo or len(memo) >= recursion_limit: 

355 raise ValueError("wrapper loop when unwrapping {!r}".format(obj)) 

356 memo[id_target] = target 

357 

358 

359# Decorator for implementing thread synchronization. It can be used as a 

360# decorator, in which case the synchronization context is determined by 

361# what type of function is wrapped, or it can also be used as a context 

362# manager, where the user needs to supply the correct synchronization 

363# context. It is also possible to supply an object which appears to be a 

364# synchronization primitive of some sort, by virtue of having release() 

365# and acquire() methods. In that case that will be used directly as the 

366# synchronization primitive without creating a separate lock against the 

367# derived or supplied context. 

368 

369 

370def synchronized(wrapped): 

371 """Depending on the nature of the `wrapped` object, will either return a 

372 decorator which can be used to wrap a function or method, or a context 

373 manager, both of which will act accordingly depending on how used, to 

374 synchronize access to calling of the wrapped function, or the block of 

375 code within the context manager. If it is an object which is a 

376 synchronization primitive, such as a threading Lock, RLock, Semaphore, 

377 Condition, or Event, then it is assumed that the object is to be used 

378 directly as the synchronization primitive, otherwise a lock is created 

379 automatically and attached to the wrapped object and used as the 

380 synchronization primitive. 

381 

382 Async functions are supported: if the wrapped callable is an async 

383 function, an `asyncio.Lock` is created for the context and the wrapper 

384 awaits the lock. The returned object also exposes `__aenter__` and 

385 `__aexit__` so it can be used with `async with` to synchronise a block 

386 of code using an independent per-context `asyncio.Lock`. If an object 

387 with coroutine `acquire`/`release` methods (such as an `asyncio.Lock`) 

388 is supplied directly, the returned decorator and context manager will 

389 use it via the async protocol. 

390 """ 

391 

392 # Determine if being passed an object which is a synchronization 

393 # primitive. We can't check by type for Lock, RLock, Semaphore etc, 

394 # as the means of creating them isn't the type. Therefore use the 

395 # existence of acquire() and release() methods. This is more 

396 # extensible anyway as it allows custom synchronization mechanisms. 

397 

398 if hasattr(wrapped, "acquire") and hasattr(wrapped, "release"): 

399 # We remember what the original lock is and then return a new 

400 # decorator which accesses and locks it. When returning the new 

401 # decorator we wrap it with an object proxy so we can override 

402 # the context manager methods in case it is being used to wrap 

403 # synchronized statements with a 'with' statement. 

404 

405 lock = wrapped 

406 

407 if _synchronized_is_async_lock(lock): 

408 

409 @decorator 

410 async def _synchronized(wrapped, instance, args, kwargs): 

411 async with lock: 

412 return await wrapped(*args, **kwargs) 

413 

414 class _AsyncSynchronizedLockProxy(CallableObjectProxy): 

415 

416 async def __aenter__(self): 

417 await lock.acquire() 

418 return lock 

419 

420 async def __aexit__(self, *args): 

421 lock.release() 

422 

423 return _AsyncSynchronizedLockProxy(wrapped=_synchronized) 

424 

425 @decorator 

426 def _synchronized(wrapped, instance, args, kwargs): 

427 # Execute the wrapped function while the original supplied 

428 # lock is held. 

429 

430 with lock: 

431 return wrapped(*args, **kwargs) 

432 

433 class _SynchronizedLockProxy(CallableObjectProxy): 

434 

435 def __enter__(self): 

436 lock.acquire() 

437 return lock 

438 

439 def __exit__(self, *args): 

440 lock.release() 

441 

442 return _SynchronizedLockProxy(wrapped=_synchronized) 

443 

444 # Following only apply when the lock is being created automatically 

445 # based on the context of what was supplied. In this case we supply 

446 # a final decorator, but need to use FunctionWrapper directly as we 

447 # want to derive from it to add context manager methods in case it is 

448 # being used to wrap synchronized statements with a 'with' statement. 

449 

450 def _synchronized_lock(context): 

451 # Attempt to retrieve the lock for the specific context. 

452 

453 lock = vars(context).get("_synchronized_lock", None) 

454 

455 if lock is None: 

456 # There is no existing lock defined for the context we 

457 # are dealing with so we need to create one. This needs 

458 # to be done in a way to guarantee there is only one 

459 # created, even if multiple threads try and create it at 

460 # the same time. We can't always use the setdefault() 

461 # method on the __dict__ for the context. This is the 

462 # case where the context is a class, as __dict__ is 

463 # actually a dictproxy. What we therefore do is use a 

464 # meta lock on this wrapper itself, to control the 

465 # creation and assignment of the lock attribute against 

466 # the context. 

467 

468 with synchronized._synchronized_meta_lock: 

469 # We need to check again for whether the lock we want 

470 # exists in case two threads were trying to create it 

471 # at the same time and were competing to create the 

472 # meta lock. 

473 

474 lock = vars(context).get("_synchronized_lock", None) 

475 

476 if lock is None: 

477 lock = RLock() 

478 setattr(context, "_synchronized_lock", lock) 

479 

480 return lock 

481 

482 def _synchronized_async_lock(context): 

483 # Per-context asyncio.Lock, created lazily on first use. Created 

484 # under the shared meta lock so creation is safe across threads; 

485 # the meta lock is never held across an await. asyncio.Lock is 

486 # not reentrant. 

487 

488 lock = vars(context).get("_synchronized_async_lock", None) 

489 

490 if lock is None: 

491 with synchronized._synchronized_meta_lock: 

492 lock = vars(context).get("_synchronized_async_lock", None) 

493 

494 if lock is None: 

495 lock = asyncio.Lock() 

496 setattr(context, "_synchronized_async_lock", lock) 

497 

498 return lock 

499 

500 def _synchronized_wrapper(wrapped, instance, args, kwargs): 

501 # Execute the wrapped function while the lock for the 

502 # desired context is held. If instance is None then the 

503 # wrapped function is used as the context. 

504 

505 with _synchronized_lock(instance if instance is not None else wrapped): 

506 return wrapped(*args, **kwargs) 

507 

508 async def _synchronized_async_wrapper(wrapped, instance, args, kwargs): 

509 async with _synchronized_async_lock( 

510 instance if instance is not None else wrapped 

511 ): 

512 return await wrapped(*args, **kwargs) 

513 

514 class _SynchronizedFunctionWrapper(FunctionWrapper): 

515 

516 def __enter__(self): 

517 self._self_lock = _synchronized_lock(self.__wrapped__) 

518 self._self_lock.acquire() 

519 return self._self_lock 

520 

521 def __exit__(self, *args): 

522 self._self_lock.release() 

523 

524 async def __aenter__(self): 

525 self._self_async_lock = _synchronized_async_lock(self.__wrapped__) 

526 await self._self_async_lock.acquire() 

527 return self._self_async_lock 

528 

529 async def __aexit__(self, *args): 

530 self._self_async_lock.release() 

531 

532 if _synchronized_is_async_callable(wrapped): 

533 return _SynchronizedFunctionWrapper( 

534 wrapped=wrapped, wrapper=_synchronized_async_wrapper 

535 ) 

536 

537 return _SynchronizedFunctionWrapper(wrapped=wrapped, wrapper=_synchronized_wrapper) 

538 

539 

540synchronized._synchronized_meta_lock = Lock() # type: ignore[attr-defined]