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

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

274 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 contextvars import Context 

21from dataclasses import dataclass, field 

22from functools import partial 

23from inspect import isawaitable 

24from threading import Lock, Thread, current_thread, get_ident 

25from types import TracebackType 

26from typing import ( 

27 Any, 

28 Generic, 

29 TypeVar, 

30 cast, 

31 overload, 

32) 

33 

34from ._core._eventloop import ( 

35 get_cancelled_exc_class, 

36 threadlocals, 

37) 

38from ._core._eventloop import run as run_eventloop 

39from ._core._exceptions import NoEventLoopError, RunFinishedError 

40from ._core._synchronization import Event 

41from ._core._tasks import CancelScope, create_task_group 

42from .abc._tasks import TaskStatus 

43from .lowlevel import EventLoopToken, current_token 

44 

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

46 from typing import Self, TypeVarTuple, Unpack 

47else: 

48 from typing_extensions import Self, TypeVarTuple, Unpack 

49 

50T_Retval = TypeVar("T_Retval") 

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

52PosArgsT = TypeVarTuple("PosArgsT") 

53 

54 

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

56 if token is not None: 

57 return token 

58 

59 try: 

60 return threadlocals.current_token 

61 except AttributeError: 

62 raise NoEventLoopError( 

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

64 "provided" 

65 ) from None 

66 

67 

68def run( 

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

70 *args: Unpack[PosArgsT], 

71 token: EventLoopToken | None = None, 

72) -> T_co: 

73 """ 

74 Call a coroutine function from a worker thread. 

75 

76 :param func: a coroutine function 

77 :param args: positional arguments for the callable 

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

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

80 :return: the return value of the coroutine function 

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

82 AnyIO worker thread 

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

84 

85 .. versionchanged:: 4.11.0 

86 Added the ``token`` parameter. 

87 

88 """ 

89 explicit_token = token is not None 

90 token = _token_or_error(token) 

91 return token.backend_class.run_async_from_thread( 

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

93 ) 

94 

95 

96def run_sync( 

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

98 *args: Unpack[PosArgsT], 

99 token: EventLoopToken | None = None, 

100) -> T_Retval: 

101 """ 

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

103 

104 :param func: a callable 

105 :param args: positional arguments for the callable 

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

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

108 :return: the return value of the callable 

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

110 AnyIO worker thread 

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

112 

113 .. versionchanged:: 4.11.0 

114 Added the ``token`` parameter. 

115 

116 """ 

117 explicit_token = token is not None 

118 token = _token_or_error(token) 

119 return token.backend_class.run_sync_from_thread( 

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

121 ) 

122 

123 

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

125 _enter_future: Future[T_co] 

126 _exit_future: Future[bool | None] 

127 _exit_event: Event 

128 _exit_exc_info: tuple[ 

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

130 ] = (None, None, None) 

131 

132 def __init__( 

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

134 ): 

135 self._async_cm = async_cm 

136 self._portal = portal 

137 

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

139 try: 

140 self._exit_event = Event() 

141 value = await self._async_cm.__aenter__() 

142 except BaseException as exc: 

143 self._enter_future.set_exception(exc) 

144 raise 

145 else: 

146 self._enter_future.set_result(value) 

147 

148 try: 

149 # Wait for the sync context manager to exit. 

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

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

152 # manager. 

153 await self._exit_event.wait() 

154 finally: 

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

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

157 # `_exit_exc_info` has been set. 

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

159 

160 return result 

161 

162 def __enter__(self) -> T_co: 

163 self._enter_future = Future() 

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

165 return self._enter_future.result() 

166 

167 def __exit__( 

168 self, 

169 exc_type: type[BaseException] | None, 

170 exc_value: BaseException | None, 

171 traceback: TracebackType | None, 

172 /, 

173 ) -> bool | None: 

174 self._exit_exc_info = exc_type, exc_value, traceback 

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

176 return self._exit_future.result() 

177 

178 

179class _BlockingPortalTaskStatus(TaskStatus): 

180 def __init__(self, future: Future): 

181 self._future = future 

182 

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

184 self._future.set_result(value) 

185 

186 

187class BlockingPortal: 

188 """ 

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

190 

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

192 current thread 

193 """ 

194 

195 def __init__(self) -> None: 

196 self._token = current_token() 

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

198 self._stop_event = Event() 

199 self._task_group = create_task_group() 

200 

201 async def __aenter__(self) -> Self: 

202 await self._task_group.__aenter__() 

203 return self 

204 

205 async def __aexit__( 

206 self, 

207 exc_type: type[BaseException] | None, 

208 exc_val: BaseException | None, 

209 exc_tb: TracebackType | None, 

210 ) -> bool: 

211 await self.stop() 

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

213 

214 def _check_running(self) -> None: 

215 if self._event_loop_thread_id is None: 

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

217 if self._event_loop_thread_id == get_ident(): 

218 raise RuntimeError( 

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

220 ) 

221 

222 async def sleep_until_stopped(self) -> None: 

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

224 await self._stop_event.wait() 

225 

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

227 """ 

228 Signal the portal to shut down. 

229 

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

231 :meth:`sleep_until_stopped`. 

232 

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

234 to let them finish before returning 

235 

236 """ 

237 self._event_loop_thread_id = None 

238 self._stop_event.set() 

239 if cancel_remaining: 

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

241 

242 async def _call_func( 

243 self, 

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

245 args: tuple[Unpack[PosArgsT]], 

246 kwargs: dict[str, Any], 

247 future: Future[T_Retval], 

248 ) -> None: 

249 event_loop_thread_id = self._event_loop_thread_id 

250 

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

252 if f.cancelled(): 

253 if event_loop_thread_id == get_ident(): 

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

255 elif event_loop_thread_id is not None: 

256 try: 

257 self._token.backend_class.run_sync_soon( 

258 scope.cancel, 

259 ("the future was cancelled",), 

260 self._token.native_token, 

261 ) 

262 except RunFinishedError: 

263 # The target event loop is gone, so there's nothing left to 

264 # cancel or notify 

265 pass 

266 

267 try: 

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

269 if isawaitable(retval_or_awaitable): 

270 with CancelScope() as scope: 

271 future.add_done_callback(callback) 

272 retval = await retval_or_awaitable 

273 else: 

274 retval = retval_or_awaitable 

275 except get_cancelled_exc_class(): 

276 future.cancel() 

277 future.set_running_or_notify_cancel() 

278 except BaseException as exc: 

279 if future.set_running_or_notify_cancel(): 

280 future.set_exception(exc) 

281 

282 # Let base exceptions fall through 

283 if not isinstance(exc, Exception): 

284 raise 

285 else: 

286 if future.set_running_or_notify_cancel(): 

287 future.set_result(retval) 

288 finally: 

289 scope = None # type: ignore[assignment] 

290 

291 def _spawn_task_from_thread( 

292 self, 

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

294 args: tuple[Unpack[PosArgsT]], 

295 kwargs: dict[str, Any], 

296 name: object, 

297 future: Future[T_Retval], 

298 ) -> None: 

299 """ 

300 Spawn a new task using the given callable. 

301 

302 :param func: a callable 

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

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

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

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

307 or the exception raised during its execution 

308 

309 """ 

310 run_sync( 

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

312 self._call_func, 

313 func, 

314 args, 

315 kwargs, 

316 future, 

317 token=self._token, 

318 ) 

319 

320 @overload 

321 def call( 

322 self, 

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

324 *args: Unpack[PosArgsT], 

325 ) -> T_Retval: ... 

326 

327 @overload 

328 def call( 

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

330 ) -> T_Retval: ... 

331 

332 def call( 

333 self, 

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

335 *args: Unpack[PosArgsT], 

336 ) -> T_Retval: 

337 """ 

338 Call the given function in the event loop thread. 

339 

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

341 

342 :param func: any callable 

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

344 from within the event loop thread 

345 

346 """ 

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

348 

349 @overload 

350 async def call_async( 

351 self, 

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

353 *args: Unpack[PosArgsT], 

354 abandon_on_cancel: bool = False, 

355 ) -> T_Retval: ... 

356 

357 @overload 

358 async def call_async( 

359 self, 

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

361 *args: Unpack[PosArgsT], 

362 abandon_on_cancel: bool = False, 

363 ) -> T_Retval: ... 

364 

365 async def call_async( 

366 self, 

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

368 *args: Unpack[PosArgsT], 

369 abandon_on_cancel: bool = False, 

370 ) -> T_Retval: 

371 """ 

372 Call the given function in the portal's event loop from another event loop. 

373 

374 If the callable returns an awaitable, it is awaited on. Cancelling the calling 

375 task cancels the task running in the portal. 

376 

377 :param func: any callable 

378 :param abandon_on_cancel: if ``False``, wait for the cancelled task to finish 

379 and propagate any exception raised during cleanup, or otherwise re-raise 

380 the caller's cancellation; if ``True``, stop waiting immediately and ignore 

381 the task's subsequent return value or exception 

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

383 from within the portal's event loop thread 

384 

385 .. versionadded:: 4.16.0 

386 """ 

387 self._check_running() 

388 source_token = current_token() 

389 event = Event() 

390 future: Future[T_Retval] = Future() 

391 cancellation: Future[None] = Future() 

392 

393 async def run_call() -> None: 

394 with CancelScope() as scope: 

395 

396 def cancel_target(_: Future[None]) -> None: 

397 try: 

398 self._token.backend_class.run_sync_soon( 

399 scope.cancel, 

400 ("the calling task was cancelled",), 

401 self._token.native_token, 

402 ) 

403 except RunFinishedError: 

404 pass 

405 

406 cancellation.add_done_callback(cancel_target) 

407 await self._call_func(func, args, {}, future) 

408 

409 def notify_source(_: Future[T_Retval]) -> None: 

410 try: 

411 source_token.backend_class.run_sync_soon( 

412 event.set, (), source_token.native_token 

413 ) 

414 except RunFinishedError: 

415 pass 

416 

417 def spawn_task() -> None: 

418 try: 

419 self._task_group.start_soon(run_call) 

420 except BaseException as exc: 

421 if not future.done(): 

422 future.set_exception(exc) 

423 

424 future.add_done_callback(notify_source) 

425 self._token.backend_class.run_sync_soon( 

426 spawn_task, (), self._token.native_token 

427 ) 

428 try: 

429 await event.wait() 

430 except get_cancelled_exc_class(): 

431 cancellation.cancel() 

432 if not abandon_on_cancel: 

433 with CancelScope(shield=True): 

434 await event.wait() 

435 

436 if not future.cancelled(): 

437 future.result() 

438 

439 raise 

440 

441 return future.result() 

442 

443 @overload 

444 def start_task_soon( 

445 self, 

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

447 *args: Unpack[PosArgsT], 

448 name: object = None, 

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

450 

451 @overload 

452 def start_task_soon( 

453 self, 

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

455 *args: Unpack[PosArgsT], 

456 name: object = None, 

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

458 

459 def start_task_soon( 

460 self, 

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

462 *args: Unpack[PosArgsT], 

463 name: object = None, 

464 ) -> Future[T_Retval]: 

465 """ 

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

467 

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

469 the returned future. 

470 

471 :param func: the target function 

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

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

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

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

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

477 from within the event loop thread 

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

479 

480 .. versionadded:: 3.0 

481 

482 """ 

483 self._check_running() 

484 f: Future[T_Retval] = Future() 

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

486 return f 

487 

488 def start_task( 

489 self, 

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

491 *args: object, 

492 name: object = None, 

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

494 """ 

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

496 

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

498 

499 :param func: the target function 

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

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

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

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

504 function 

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

506 

507 .. versionadded:: 3.0 

508 

509 """ 

510 

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

512 if not task_status_future.done(): 

513 if future.cancelled(): 

514 task_status_future.cancel() 

515 elif future.exception(): 

516 task_status_future.set_exception(future.exception()) 

517 else: 

518 exc = RuntimeError( 

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

520 ) 

521 task_status_future.set_exception(exc) 

522 

523 self._check_running() 

524 task_status_future: Future = Future() 

525 task_status = _BlockingPortalTaskStatus(task_status_future) 

526 f: Future = Future() 

527 f.add_done_callback(task_done) 

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

529 return f, task_status_future.result() 

530 

531 def wrap_async_context_manager( 

532 self, cm: AbstractAsyncContextManager[T_co] 

533 ) -> AbstractContextManager[T_co]: 

534 """ 

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

536 

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

538 in the middle until the synchronous context manager exits. 

539 

540 :param cm: an asynchronous context manager 

541 :return: a synchronous context manager 

542 

543 .. versionadded:: 2.1 

544 

545 """ 

546 return _BlockingAsyncContextManager(cm, self) 

547 

548 

549@dataclass 

550class BlockingPortalProvider: 

551 """ 

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

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

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

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

556 least one thread has entered this context manager. 

557 

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

559 

560 :param backend: name of the backend 

561 :param backend_options: backend options 

562 

563 .. versionadded:: 4.4 

564 """ 

565 

566 backend: str = "asyncio" 

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

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

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

570 _portal: BlockingPortal = field(init=False) 

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

572 init=False, default=None 

573 ) 

574 

575 def __enter__(self) -> BlockingPortal: 

576 with self._lock: 

577 if self._portal_cm is None: 

578 portal_cm = start_blocking_portal(self.backend, self.backend_options) 

579 portal = portal_cm.__enter__() 

580 self._portal_cm = portal_cm 

581 self._portal = portal 

582 

583 self._leases += 1 

584 return self._portal 

585 

586 def __exit__( 

587 self, 

588 exc_type: type[BaseException] | None, 

589 exc_val: BaseException | None, 

590 exc_tb: TracebackType | None, 

591 ) -> None: 

592 portal_cm: AbstractContextManager[BlockingPortal] | None = None 

593 with self._lock: 

594 assert self._portal_cm 

595 assert self._leases > 0 

596 self._leases -= 1 

597 if not self._leases: 

598 portal_cm = self._portal_cm 

599 self._portal_cm = None 

600 del self._portal 

601 

602 if portal_cm: 

603 portal_cm.__exit__(exc_type, exc_val, exc_tb) 

604 

605 

606@contextmanager 

607def start_blocking_portal( 

608 backend: str = "asyncio", 

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

610 *, 

611 name: str | None = None, 

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

613 """ 

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

615 

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

617 

618 :param backend: name of the backend 

619 :param backend_options: backend options 

620 :param name: name of the thread 

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

622 

623 .. versionchanged:: 3.0 

624 Usage as a context manager is now required. 

625 

626 """ 

627 

628 async def run_portal() -> None: 

629 async with BlockingPortal() as portal_: 

630 if name is None: 

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

632 

633 future.set_result(portal_) 

634 await portal_.sleep_until_stopped() 

635 

636 def run_blocking_portal() -> None: 

637 if future.set_running_or_notify_cancel(): 

638 try: 

639 run_eventloop( 

640 run_portal, backend=backend, backend_options=backend_options 

641 ) 

642 except BaseException as exc: 

643 if not future.done(): 

644 future.set_exception(exc) 

645 

646 future: Future[BlockingPortal] = Future() 

647 kwargs: dict[str, Any] = {} 

648 if sys.version_info >= (3, 14): 

649 kwargs["context"] = Context() 

650 

651 thread = Thread(target=run_blocking_portal, daemon=True, name=name, **kwargs) 

652 thread.start() 

653 try: 

654 cancel_remaining_tasks = False 

655 portal = future.result() 

656 try: 

657 yield portal 

658 except BaseException: 

659 cancel_remaining_tasks = True 

660 raise 

661 finally: 

662 try: 

663 portal.call(portal.stop, cancel_remaining_tasks) 

664 except RuntimeError: 

665 pass 

666 finally: 

667 thread.join() 

668 

669 

670def check_cancelled() -> None: 

671 """ 

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

673 been cancelled. 

674 

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

676 backend-specific cancellation exception will be raised. 

677 

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

679 :func:`.to_thread.run_sync` 

680 

681 """ 

682 try: 

683 token: EventLoopToken = threadlocals.current_token 

684 except AttributeError: 

685 raise NoEventLoopError( 

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

687 ) from None 

688 

689 token.backend_class.check_cancelled()