Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/anyio/from_thread.py: 33%

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

222 statements  

1from __future__ import annotations 

2 

3__all__ = ( 

4 "BlockingPortal", 

5 "BlockingPortalProvider", 

6 "check_cancelled", 

7 "run", 

8 "run_sync", 

9 "start_blocking_portal", 

10) 

11 

12import sys 

13from collections.abc import Awaitable, Callable, Coroutine, Generator 

14from concurrent.futures import Future 

15from contextlib import ( 

16 AbstractAsyncContextManager, 

17 AbstractContextManager, 

18 contextmanager, 

19) 

20from dataclasses import dataclass, field 

21from functools import partial 

22from inspect import isawaitable 

23from threading import Lock, Thread, current_thread, get_ident 

24from types import TracebackType 

25from typing import ( 

26 Any, 

27 Generic, 

28 TypeVar, 

29 cast, 

30 overload, 

31) 

32 

33from ._core._eventloop import ( 

34 get_cancelled_exc_class, 

35 threadlocals, 

36) 

37from ._core._eventloop import run as run_eventloop 

38from ._core._exceptions import NoEventLoopError 

39from ._core._synchronization import Event 

40from ._core._tasks import CancelScope, create_task_group 

41from .abc._tasks import TaskStatus 

42from .lowlevel import EventLoopToken, current_token 

43 

44if sys.version_info >= (3, 11): 

45 from typing import TypeVarTuple, Unpack 

46else: 

47 from typing_extensions import TypeVarTuple, Unpack 

48 

49T_Retval = TypeVar("T_Retval") 

50T_co = TypeVar("T_co", covariant=True) 

51PosArgsT = TypeVarTuple("PosArgsT") 

52 

53 

54def _token_or_error(token: EventLoopToken | None) -> EventLoopToken: 

55 if token is not None: 

56 return token 

57 

58 try: 

59 return threadlocals.current_token 

60 except AttributeError: 

61 raise NoEventLoopError( 

62 "Not running inside an AnyIO worker thread, and no event loop token was " 

63 "provided" 

64 ) from None 

65 

66 

67def run( 

68 func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], 

69 *args: Unpack[PosArgsT], 

70 token: EventLoopToken | None = None, 

71) -> T_co: 

72 """ 

73 Call a coroutine function from a worker thread. 

74 

75 :param func: a coroutine function 

76 :param args: positional arguments for the callable 

77 :param token: an event loop token to use to get back to the event loop thread 

78 (required if calling this function from outside an AnyIO worker thread) 

79 :return: the return value of the coroutine function 

80 :raises MissingTokenError: if no token was provided and called from outside an 

81 AnyIO worker thread 

82 :raises RunFinishedError: if the event loop tied to ``token`` is no longer running 

83 

84 .. versionchanged:: 4.11.0 

85 Added the ``token`` parameter. 

86 

87 """ 

88 explicit_token = token is not None 

89 token = _token_or_error(token) 

90 return token.backend_class.run_async_from_thread( 

91 func, args, token=token.native_token if explicit_token else None 

92 ) 

93 

94 

95def run_sync( 

96 func: Callable[[Unpack[PosArgsT]], T_Retval], 

97 *args: Unpack[PosArgsT], 

98 token: EventLoopToken | None = None, 

99) -> T_Retval: 

100 """ 

101 Call a function in the event loop thread from a worker thread. 

102 

103 :param func: a callable 

104 :param args: positional arguments for the callable 

105 :param token: an event loop token to use to get back to the event loop thread 

106 (required if calling this function from outside an AnyIO worker thread) 

107 :return: the return value of the callable 

108 :raises MissingTokenError: if no token was provided and called from outside an 

109 AnyIO worker thread 

110 :raises RunFinishedError: if the event loop tied to ``token`` is no longer running 

111 

112 .. versionchanged:: 4.11.0 

113 Added the ``token`` parameter. 

114 

115 """ 

116 explicit_token = token is not None 

117 token = _token_or_error(token) 

118 return token.backend_class.run_sync_from_thread( 

119 func, args, token=token.native_token if explicit_token else None 

120 ) 

121 

122 

123class _BlockingAsyncContextManager(Generic[T_co], AbstractContextManager): 

124 _enter_future: Future[T_co] 

125 _exit_future: Future[bool | None] 

126 _exit_event: Event 

127 _exit_exc_info: tuple[ 

128 type[BaseException] | None, BaseException | None, TracebackType | None 

129 ] = (None, None, None) 

130 

131 def __init__( 

132 self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal 

133 ): 

134 self._async_cm = async_cm 

135 self._portal = portal 

136 

137 async def run_async_cm(self) -> bool | None: 

138 try: 

139 self._exit_event = Event() 

140 value = await self._async_cm.__aenter__() 

141 except BaseException as exc: 

142 self._enter_future.set_exception(exc) 

143 raise 

144 else: 

145 self._enter_future.set_result(value) 

146 

147 try: 

148 # Wait for the sync context manager to exit. 

149 # This next statement can raise `get_cancelled_exc_class()` if 

150 # something went wrong in a task group in this async context 

151 # manager. 

152 await self._exit_event.wait() 

153 finally: 

154 # In case of cancellation, it could be that we end up here before 

155 # `_BlockingAsyncContextManager.__exit__` is called, and an 

156 # `_exit_exc_info` has been set. 

157 result = await self._async_cm.__aexit__(*self._exit_exc_info) 

158 

159 return result 

160 

161 def __enter__(self) -> T_co: 

162 self._enter_future = Future() 

163 self._exit_future = self._portal.start_task_soon(self.run_async_cm) 

164 return self._enter_future.result() 

165 

166 def __exit__( 

167 self, 

168 __exc_type: type[BaseException] | None, 

169 __exc_value: BaseException | None, 

170 __traceback: TracebackType | None, 

171 ) -> bool | None: 

172 self._exit_exc_info = __exc_type, __exc_value, __traceback 

173 self._portal.call(self._exit_event.set) 

174 return self._exit_future.result() 

175 

176 

177class _BlockingPortalTaskStatus(TaskStatus): 

178 def __init__(self, future: Future): 

179 self._future = future 

180 

181 def started(self, value: object = None) -> None: 

182 self._future.set_result(value) 

183 

184 

185class BlockingPortal: 

186 """ 

187 An object that lets external threads run code in an asynchronous event loop. 

188 

189 :raises NoEventLoopError: if no supported asynchronous event loop is running in the 

190 current thread 

191 """ 

192 

193 def __init__(self) -> None: 

194 self._token = current_token() 

195 self._event_loop_thread_id: int | None = get_ident() 

196 self._stop_event = Event() 

197 self._task_group = create_task_group() 

198 

199 async def __aenter__(self) -> BlockingPortal: 

200 await self._task_group.__aenter__() 

201 return self 

202 

203 async def __aexit__( 

204 self, 

205 exc_type: type[BaseException] | None, 

206 exc_val: BaseException | None, 

207 exc_tb: TracebackType | None, 

208 ) -> bool: 

209 await self.stop() 

210 return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) 

211 

212 def _check_running(self) -> None: 

213 if self._event_loop_thread_id is None: 

214 raise RuntimeError("This portal is not running") 

215 if self._event_loop_thread_id == get_ident(): 

216 raise RuntimeError( 

217 "This method cannot be called from the event loop thread" 

218 ) 

219 

220 async def sleep_until_stopped(self) -> None: 

221 """Sleep until :meth:`stop` is called.""" 

222 await self._stop_event.wait() 

223 

224 async def stop(self, cancel_remaining: bool = False) -> None: 

225 """ 

226 Signal the portal to shut down. 

227 

228 This marks the portal as no longer accepting new calls and exits from 

229 :meth:`sleep_until_stopped`. 

230 

231 :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False`` 

232 to let them finish before returning 

233 

234 """ 

235 self._event_loop_thread_id = None 

236 self._stop_event.set() 

237 if cancel_remaining: 

238 self._task_group.cancel_scope.cancel("the blocking portal is shutting down") 

239 

240 async def _call_func( 

241 self, 

242 func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], 

243 args: tuple[Unpack[PosArgsT]], 

244 kwargs: dict[str, Any], 

245 future: Future[T_Retval], 

246 ) -> None: 

247 event_loop_thread_id = self._event_loop_thread_id 

248 

249 def callback(f: Future[T_Retval]) -> None: 

250 if f.cancelled(): 

251 if event_loop_thread_id == get_ident(): 

252 scope.cancel("the future was cancelled") 

253 elif event_loop_thread_id is not None: 

254 run_sync( 

255 scope.cancel, "the future was cancelled", token=self._token 

256 ) 

257 

258 try: 

259 retval_or_awaitable = func(*args, **kwargs) 

260 if isawaitable(retval_or_awaitable): 

261 with CancelScope() as scope: 

262 future.add_done_callback(callback) 

263 retval = await retval_or_awaitable 

264 else: 

265 retval = retval_or_awaitable 

266 except get_cancelled_exc_class(): 

267 future.cancel() 

268 future.set_running_or_notify_cancel() 

269 except BaseException as exc: 

270 if not future.cancelled(): 

271 future.set_exception(exc) 

272 

273 # Let base exceptions fall through 

274 if not isinstance(exc, Exception): 

275 raise 

276 else: 

277 if not future.cancelled(): 

278 future.set_result(retval) 

279 finally: 

280 scope = None # type: ignore[assignment] 

281 

282 def _spawn_task_from_thread( 

283 self, 

284 func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], 

285 args: tuple[Unpack[PosArgsT]], 

286 kwargs: dict[str, Any], 

287 name: object, 

288 future: Future[T_Retval], 

289 ) -> None: 

290 """ 

291 Spawn a new task using the given callable. 

292 

293 :param func: a callable 

294 :param args: positional arguments to be passed to the callable 

295 :param kwargs: keyword arguments to be passed to the callable 

296 :param name: name of the task (will be coerced to a string if not ``None``) 

297 :param future: a future that will resolve to the return value of the callable, 

298 or the exception raised during its execution 

299 

300 """ 

301 run_sync( 

302 partial(self._task_group.start_soon, name=name), 

303 self._call_func, 

304 func, 

305 args, 

306 kwargs, 

307 future, 

308 token=self._token, 

309 ) 

310 

311 @overload 

312 def call( 

313 self, 

314 func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], 

315 *args: Unpack[PosArgsT], 

316 ) -> T_Retval: ... 

317 

318 @overload 

319 def call( 

320 self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] 

321 ) -> T_Retval: ... 

322 

323 def call( 

324 self, 

325 func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], 

326 *args: Unpack[PosArgsT], 

327 ) -> T_Retval: 

328 """ 

329 Call the given function in the event loop thread. 

330 

331 If the callable returns a coroutine object, it is awaited on. 

332 

333 :param func: any callable 

334 :raises RuntimeError: if the portal is not running or if this method is called 

335 from within the event loop thread 

336 

337 """ 

338 return cast(T_Retval, self.start_task_soon(func, *args).result()) 

339 

340 @overload 

341 def start_task_soon( 

342 self, 

343 func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], 

344 *args: Unpack[PosArgsT], 

345 name: object = None, 

346 ) -> Future[T_Retval]: ... 

347 

348 @overload 

349 def start_task_soon( 

350 self, 

351 func: Callable[[Unpack[PosArgsT]], T_Retval], 

352 *args: Unpack[PosArgsT], 

353 name: object = None, 

354 ) -> Future[T_Retval]: ... 

355 

356 def start_task_soon( 

357 self, 

358 func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], 

359 *args: Unpack[PosArgsT], 

360 name: object = None, 

361 ) -> Future[T_Retval]: 

362 """ 

363 Start a task in the portal's task group. 

364 

365 The task will be run inside a cancel scope which can be cancelled by cancelling 

366 the returned future. 

367 

368 :param func: the target function 

369 :param args: positional arguments passed to ``func`` 

370 :param name: name of the task (will be coerced to a string if not ``None``) 

371 :return: a future that resolves with the return value of the callable if the 

372 task completes successfully, or with the exception raised in the task 

373 :raises RuntimeError: if the portal is not running or if this method is called 

374 from within the event loop thread 

375 :rtype: concurrent.futures.Future[T_Retval] 

376 

377 .. versionadded:: 3.0 

378 

379 """ 

380 self._check_running() 

381 f: Future[T_Retval] = Future() 

382 self._spawn_task_from_thread(func, args, {}, name, f) 

383 return f 

384 

385 def start_task( 

386 self, 

387 func: Callable[..., Awaitable[T_Retval]], 

388 *args: object, 

389 name: object = None, 

390 ) -> tuple[Future[T_Retval], Any]: 

391 """ 

392 Start a task in the portal's task group and wait until it signals for readiness. 

393 

394 This method works the same way as :meth:`.abc.TaskGroup.start`. 

395 

396 :param func: the target function 

397 :param args: positional arguments passed to ``func`` 

398 :param name: name of the task (will be coerced to a string if not ``None``) 

399 :return: a tuple of (future, task_status_value) where the ``task_status_value`` 

400 is the value passed to ``task_status.started()`` from within the target 

401 function 

402 :rtype: tuple[concurrent.futures.Future[T_Retval], Any] 

403 

404 .. versionadded:: 3.0 

405 

406 """ 

407 

408 def task_done(future: Future[T_Retval]) -> None: 

409 if not task_status_future.done(): 

410 if future.cancelled(): 

411 task_status_future.cancel() 

412 elif future.exception(): 

413 task_status_future.set_exception(future.exception()) 

414 else: 

415 exc = RuntimeError( 

416 "Task exited without calling task_status.started()" 

417 ) 

418 task_status_future.set_exception(exc) 

419 

420 self._check_running() 

421 task_status_future: Future = Future() 

422 task_status = _BlockingPortalTaskStatus(task_status_future) 

423 f: Future = Future() 

424 f.add_done_callback(task_done) 

425 self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f) 

426 return f, task_status_future.result() 

427 

428 def wrap_async_context_manager( 

429 self, cm: AbstractAsyncContextManager[T_co] 

430 ) -> AbstractContextManager[T_co]: 

431 """ 

432 Wrap an async context manager as a synchronous context manager via this portal. 

433 

434 Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping 

435 in the middle until the synchronous context manager exits. 

436 

437 :param cm: an asynchronous context manager 

438 :return: a synchronous context manager 

439 

440 .. versionadded:: 2.1 

441 

442 """ 

443 return _BlockingAsyncContextManager(cm, self) 

444 

445 

446@dataclass 

447class BlockingPortalProvider: 

448 """ 

449 A manager for a blocking portal. Used as a context manager. The first thread to 

450 enter this context manager causes a blocking portal to be started with the specific 

451 parameters, and the last thread to exit causes the portal to be shut down. Thus, 

452 there will be exactly one blocking portal running in this context as long as at 

453 least one thread has entered this context manager. 

454 

455 The parameters are the same as for :func:`~anyio.run`. 

456 

457 :param backend: name of the backend 

458 :param backend_options: backend options 

459 

460 .. versionadded:: 4.4 

461 """ 

462 

463 backend: str = "asyncio" 

464 backend_options: dict[str, Any] | None = None 

465 _lock: Lock = field(init=False, default_factory=Lock) 

466 _leases: int = field(init=False, default=0) 

467 _portal: BlockingPortal = field(init=False) 

468 _portal_cm: AbstractContextManager[BlockingPortal] | None = field( 

469 init=False, default=None 

470 ) 

471 

472 def __enter__(self) -> BlockingPortal: 

473 with self._lock: 

474 if self._portal_cm is None: 

475 self._portal_cm = start_blocking_portal( 

476 self.backend, self.backend_options 

477 ) 

478 self._portal = self._portal_cm.__enter__() 

479 

480 self._leases += 1 

481 return self._portal 

482 

483 def __exit__( 

484 self, 

485 exc_type: type[BaseException] | None, 

486 exc_val: BaseException | None, 

487 exc_tb: TracebackType | None, 

488 ) -> None: 

489 portal_cm: AbstractContextManager[BlockingPortal] | None = None 

490 with self._lock: 

491 assert self._portal_cm 

492 assert self._leases > 0 

493 self._leases -= 1 

494 if not self._leases: 

495 portal_cm = self._portal_cm 

496 self._portal_cm = None 

497 del self._portal 

498 

499 if portal_cm: 

500 portal_cm.__exit__(None, None, None) 

501 

502 

503@contextmanager 

504def start_blocking_portal( 

505 backend: str = "asyncio", 

506 backend_options: dict[str, Any] | None = None, 

507 *, 

508 name: str | None = None, 

509) -> Generator[BlockingPortal, Any, None]: 

510 """ 

511 Start a new event loop in a new thread and run a blocking portal in its main task. 

512 

513 The parameters are the same as for :func:`~anyio.run`. 

514 

515 :param backend: name of the backend 

516 :param backend_options: backend options 

517 :param name: name of the thread 

518 :return: a context manager that yields a blocking portal 

519 

520 .. versionchanged:: 3.0 

521 Usage as a context manager is now required. 

522 

523 """ 

524 

525 async def run_portal() -> None: 

526 async with BlockingPortal() as portal_: 

527 if name is None: 

528 current_thread().name = f"{backend}-portal-{id(portal_):x}" 

529 

530 future.set_result(portal_) 

531 await portal_.sleep_until_stopped() 

532 

533 def run_blocking_portal() -> None: 

534 if future.set_running_or_notify_cancel(): 

535 try: 

536 run_eventloop( 

537 run_portal, backend=backend, backend_options=backend_options 

538 ) 

539 except BaseException as exc: 

540 if not future.done(): 

541 future.set_exception(exc) 

542 

543 future: Future[BlockingPortal] = Future() 

544 thread = Thread(target=run_blocking_portal, daemon=True, name=name) 

545 thread.start() 

546 try: 

547 cancel_remaining_tasks = False 

548 portal = future.result() 

549 try: 

550 yield portal 

551 except BaseException: 

552 cancel_remaining_tasks = True 

553 raise 

554 finally: 

555 try: 

556 portal.call(portal.stop, cancel_remaining_tasks) 

557 except RuntimeError: 

558 pass 

559 finally: 

560 thread.join() 

561 

562 

563def check_cancelled() -> None: 

564 """ 

565 Check if the cancel scope of the host task's running the current worker thread has 

566 been cancelled. 

567 

568 If the host task's current cancel scope has indeed been cancelled, the 

569 backend-specific cancellation exception will be raised. 

570 

571 :raises RuntimeError: if the current thread was not spawned by 

572 :func:`.to_thread.run_sync` 

573 

574 """ 

575 try: 

576 token: EventLoopToken = threadlocals.current_token 

577 except AttributeError: 

578 raise NoEventLoopError( 

579 "This function can only be called inside an AnyIO worker thread" 

580 ) from None 

581 

582 token.backend_class.check_cancelled()