Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/anyio/_core/_synchronization.py: 46%

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

362 statements  

1from __future__ import annotations 

2 

3import math 

4import sys 

5from collections import deque 

6from collections.abc import Callable 

7from dataclasses import dataclass 

8from types import TracebackType 

9from typing import TypeVar 

10 

11from ..lowlevel import checkpoint_if_cancelled 

12from ._eventloop import get_async_backend 

13from ._exceptions import BusyResourceError, NoEventLoopError 

14from ._tasks import CancelScope 

15from ._testing import TaskInfo, get_current_task 

16 

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

18 from typing import Self 

19else: 

20 from typing_extensions import Self 

21 

22T = TypeVar("T") 

23 

24 

25@dataclass(frozen=True) 

26class EventStatistics: 

27 """ 

28 :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait` 

29 """ 

30 

31 tasks_waiting: int 

32 

33 

34@dataclass(frozen=True) 

35class CapacityLimiterStatistics: 

36 """ 

37 :ivar int borrowed_tokens: number of tokens currently borrowed by tasks 

38 :ivar float total_tokens: total number of available tokens 

39 :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from 

40 this limiter 

41 :ivar int tasks_waiting: number of tasks waiting on 

42 :meth:`~.CapacityLimiter.acquire` or 

43 :meth:`~.CapacityLimiter.acquire_on_behalf_of` 

44 """ 

45 

46 borrowed_tokens: int 

47 total_tokens: float 

48 borrowers: tuple[object, ...] 

49 tasks_waiting: int 

50 

51 

52@dataclass(frozen=True) 

53class LockStatistics: 

54 """ 

55 :ivar bool locked: flag indicating if this lock is locked or not 

56 :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the 

57 lock is not held by any task) 

58 :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire` 

59 """ 

60 

61 locked: bool 

62 owner: TaskInfo | None 

63 tasks_waiting: int 

64 

65 

66@dataclass(frozen=True) 

67class ConditionStatistics: 

68 """ 

69 :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait` 

70 :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying 

71 :class:`~.Lock` 

72 """ 

73 

74 tasks_waiting: int 

75 lock_statistics: LockStatistics 

76 

77 

78@dataclass(frozen=True) 

79class SemaphoreStatistics: 

80 """ 

81 :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire` 

82 

83 """ 

84 

85 tasks_waiting: int 

86 

87 

88class Event: 

89 __slots__ = ("__weakref__",) 

90 

91 def __new__(cls) -> Event: 

92 try: 

93 return get_async_backend().create_event() 

94 except NoEventLoopError: 

95 return EventAdapter() 

96 

97 def set(self) -> None: 

98 """Set the flag, notifying all listeners.""" 

99 raise NotImplementedError 

100 

101 def is_set(self) -> bool: 

102 """Return ``True`` if the flag is set, ``False`` if not.""" 

103 raise NotImplementedError 

104 

105 async def wait(self) -> None: 

106 """ 

107 Wait until the flag has been set. 

108 

109 If the flag has already been set when this method is called, it returns 

110 immediately. 

111 

112 """ 

113 raise NotImplementedError 

114 

115 def statistics(self) -> EventStatistics: 

116 """Return statistics about the current state of this event.""" 

117 raise NotImplementedError 

118 

119 

120class EventAdapter(Event): 

121 __slots__ = "_internal_event", "_is_set" 

122 

123 def __new__(cls) -> Self: 

124 return object.__new__(cls) 

125 

126 def __init__(self) -> None: 

127 self._internal_event: Event | None = None 

128 self._is_set = False 

129 

130 @property 

131 def _event(self) -> Event: 

132 if self._internal_event is None: 

133 self._internal_event = get_async_backend().create_event() 

134 if self._is_set: 

135 self._internal_event.set() 

136 

137 return self._internal_event 

138 

139 def set(self) -> None: 

140 if self._internal_event is None: 

141 self._is_set = True 

142 else: 

143 self._event.set() 

144 

145 def is_set(self) -> bool: 

146 if self._internal_event is None: 

147 return self._is_set 

148 

149 return self._internal_event.is_set() 

150 

151 async def wait(self) -> None: 

152 await self._event.wait() 

153 

154 def statistics(self) -> EventStatistics: 

155 if self._internal_event is None: 

156 return EventStatistics(tasks_waiting=0) 

157 

158 return self._internal_event.statistics() 

159 

160 

161class Lock: 

162 __slots__ = ("__weakref__",) 

163 

164 def __new__(cls, *, fast_acquire: bool = False) -> Lock: 

165 try: 

166 return get_async_backend().create_lock(fast_acquire=fast_acquire) 

167 except NoEventLoopError: 

168 return LockAdapter(fast_acquire=fast_acquire) 

169 

170 async def __aenter__(self) -> None: 

171 await self.acquire() 

172 

173 async def __aexit__( 

174 self, 

175 exc_type: type[BaseException] | None, 

176 exc_val: BaseException | None, 

177 exc_tb: TracebackType | None, 

178 ) -> None: 

179 self.release() 

180 

181 async def acquire(self) -> None: 

182 """Acquire the lock.""" 

183 raise NotImplementedError 

184 

185 def acquire_nowait(self) -> None: 

186 """ 

187 Acquire the lock, without blocking. 

188 

189 :raises ~anyio.WouldBlock: if the operation would block 

190 

191 """ 

192 raise NotImplementedError 

193 

194 def release(self) -> None: 

195 """Release the lock.""" 

196 raise NotImplementedError 

197 

198 def locked(self) -> bool: 

199 """Return True if the lock is currently held.""" 

200 raise NotImplementedError 

201 

202 def statistics(self) -> LockStatistics: 

203 """ 

204 Return statistics about the current state of this lock. 

205 

206 .. versionadded:: 3.0 

207 """ 

208 raise NotImplementedError 

209 

210 

211class LockAdapter(Lock): 

212 __slots__ = "_fast_acquire", "_internal_lock" 

213 

214 def __new__(cls, *, fast_acquire: bool = False) -> Self: 

215 return object.__new__(cls) 

216 

217 def __init__(self, *, fast_acquire: bool = False): 

218 self._internal_lock: Lock | None = None 

219 self._fast_acquire = fast_acquire 

220 

221 @property 

222 def _lock(self) -> Lock: 

223 if self._internal_lock is None: 

224 self._internal_lock = get_async_backend().create_lock( 

225 fast_acquire=self._fast_acquire 

226 ) 

227 

228 return self._internal_lock 

229 

230 async def __aenter__(self) -> None: 

231 await self._lock.acquire() 

232 

233 async def __aexit__( 

234 self, 

235 exc_type: type[BaseException] | None, 

236 exc_val: BaseException | None, 

237 exc_tb: TracebackType | None, 

238 ) -> None: 

239 if self._internal_lock is not None: 

240 self._internal_lock.release() 

241 

242 async def acquire(self) -> None: 

243 """Acquire the lock.""" 

244 await self._lock.acquire() 

245 

246 def acquire_nowait(self) -> None: 

247 """ 

248 Acquire the lock, without blocking. 

249 

250 :raises ~anyio.WouldBlock: if the operation would block 

251 

252 """ 

253 self._lock.acquire_nowait() 

254 

255 def release(self) -> None: 

256 """Release the lock.""" 

257 self._lock.release() 

258 

259 def locked(self) -> bool: 

260 """Return True if the lock is currently held.""" 

261 if self._internal_lock is None: 

262 return False 

263 

264 return self._internal_lock.locked() 

265 

266 def statistics(self) -> LockStatistics: 

267 """ 

268 Return statistics about the current state of this lock. 

269 

270 .. versionadded:: 3.0 

271 

272 """ 

273 if self._internal_lock is None: 

274 return LockStatistics(False, None, 0) 

275 

276 return self._internal_lock.statistics() 

277 

278 

279class Condition: 

280 __slots__ = "__weakref__", "_lock", "_waiters" 

281 

282 def __init__(self, lock: Lock | None = None): 

283 self._lock = lock or Lock() 

284 self._waiters: deque[Event] = deque() 

285 

286 async def __aenter__(self) -> None: 

287 await self.acquire() 

288 

289 async def __aexit__( 

290 self, 

291 exc_type: type[BaseException] | None, 

292 exc_val: BaseException | None, 

293 exc_tb: TracebackType | None, 

294 ) -> None: 

295 self.release() 

296 

297 def _check_acquired(self) -> None: 

298 if self._lock.statistics().owner != get_current_task(): 

299 raise RuntimeError("The current task is not holding the underlying lock") 

300 

301 async def acquire(self) -> None: 

302 """Acquire the underlying lock.""" 

303 await self._lock.acquire() 

304 

305 def acquire_nowait(self) -> None: 

306 """ 

307 Acquire the underlying lock, without blocking. 

308 

309 :raises ~anyio.WouldBlock: if the operation would block 

310 

311 """ 

312 self._lock.acquire_nowait() 

313 

314 def release(self) -> None: 

315 """Release the underlying lock.""" 

316 self._lock.release() 

317 

318 def locked(self) -> bool: 

319 """Return True if the lock is set.""" 

320 return self._lock.locked() 

321 

322 def notify(self, n: int = 1) -> None: 

323 """Notify exactly n listeners.""" 

324 self._check_acquired() 

325 for _ in range(n): 

326 try: 

327 event = self._waiters.popleft() 

328 except IndexError: 

329 break 

330 

331 event.set() 

332 

333 def notify_all(self) -> None: 

334 """Notify all the listeners.""" 

335 self._check_acquired() 

336 for event in self._waiters: 

337 event.set() 

338 

339 self._waiters.clear() 

340 

341 async def wait(self) -> None: 

342 """Wait for a notification.""" 

343 await checkpoint_if_cancelled() 

344 self._check_acquired() 

345 event = Event() 

346 self._waiters.append(event) 

347 self.release() 

348 try: 

349 await event.wait() 

350 except BaseException: 

351 if not event.is_set(): 

352 self._waiters.remove(event) 

353 elif self._waiters: 

354 # This task was notified by could not act on it, so pass 

355 # it on to the next task 

356 self._waiters.popleft().set() 

357 

358 raise 

359 finally: 

360 with CancelScope(shield=True): 

361 await self.acquire() 

362 

363 async def wait_for(self, predicate: Callable[[], T]) -> T: 

364 """ 

365 Wait until a predicate becomes true. 

366 

367 :param predicate: a callable that returns a truthy value when the condition is 

368 met 

369 :return: the result of the predicate 

370 

371 .. versionadded:: 4.11.0 

372 

373 """ 

374 while not (result := predicate()): 

375 await self.wait() 

376 

377 return result 

378 

379 def statistics(self) -> ConditionStatistics: 

380 """ 

381 Return statistics about the current state of this condition. 

382 

383 .. versionadded:: 3.0 

384 """ 

385 return ConditionStatistics(len(self._waiters), self._lock.statistics()) 

386 

387 

388class Semaphore: 

389 __slots__ = "__weakref__", "_fast_acquire" 

390 

391 def __new__( 

392 cls, 

393 initial_value: int, 

394 *, 

395 max_value: int | None = None, 

396 fast_acquire: bool = False, 

397 ) -> Semaphore: 

398 try: 

399 return get_async_backend().create_semaphore( 

400 initial_value, max_value=max_value, fast_acquire=fast_acquire 

401 ) 

402 except NoEventLoopError: 

403 return SemaphoreAdapter(initial_value, max_value=max_value) 

404 

405 def __init__( 

406 self, 

407 initial_value: int, 

408 *, 

409 max_value: int | None = None, 

410 fast_acquire: bool = False, 

411 ): 

412 if not isinstance(initial_value, int): 

413 raise TypeError("initial_value must be an integer") 

414 if initial_value < 0: 

415 raise ValueError("initial_value must be >= 0") 

416 if max_value is not None: 

417 if not isinstance(max_value, int): 

418 raise TypeError("max_value must be an integer or None") 

419 if max_value < initial_value: 

420 raise ValueError( 

421 "max_value must be equal to or higher than initial_value" 

422 ) 

423 

424 self._fast_acquire = fast_acquire 

425 

426 async def __aenter__(self) -> Self: 

427 await self.acquire() 

428 return self 

429 

430 async def __aexit__( 

431 self, 

432 exc_type: type[BaseException] | None, 

433 exc_val: BaseException | None, 

434 exc_tb: TracebackType | None, 

435 ) -> None: 

436 self.release() 

437 

438 async def acquire(self) -> None: 

439 """Decrement the semaphore value, blocking if necessary.""" 

440 raise NotImplementedError 

441 

442 def acquire_nowait(self) -> None: 

443 """ 

444 Acquire the underlying lock, without blocking. 

445 

446 :raises ~anyio.WouldBlock: if the operation would block 

447 

448 """ 

449 raise NotImplementedError 

450 

451 def release(self) -> None: 

452 """Increment the semaphore value.""" 

453 raise NotImplementedError 

454 

455 @property 

456 def value(self) -> int: 

457 """The current value of the semaphore.""" 

458 raise NotImplementedError 

459 

460 @property 

461 def max_value(self) -> int | None: 

462 """The maximum value of the semaphore.""" 

463 raise NotImplementedError 

464 

465 def statistics(self) -> SemaphoreStatistics: 

466 """ 

467 Return statistics about the current state of this semaphore. 

468 

469 .. versionadded:: 3.0 

470 """ 

471 raise NotImplementedError 

472 

473 

474class SemaphoreAdapter(Semaphore): 

475 __slots__ = "_initial_value", "_internal_semaphore", "_max_value" 

476 

477 def __new__( 

478 cls, 

479 initial_value: int, 

480 *, 

481 max_value: int | None = None, 

482 fast_acquire: bool = False, 

483 ) -> Self: 

484 return object.__new__(cls) 

485 

486 def __init__( 

487 self, 

488 initial_value: int, 

489 *, 

490 max_value: int | None = None, 

491 fast_acquire: bool = False, 

492 ) -> None: 

493 super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) 

494 self._internal_semaphore: Semaphore | None = None 

495 self._initial_value = initial_value 

496 self._max_value = max_value 

497 

498 @property 

499 def _semaphore(self) -> Semaphore: 

500 if self._internal_semaphore is None: 

501 self._internal_semaphore = get_async_backend().create_semaphore( 

502 self._initial_value, 

503 max_value=self._max_value, 

504 fast_acquire=self._fast_acquire, 

505 ) 

506 

507 return self._internal_semaphore 

508 

509 async def acquire(self) -> None: 

510 await self._semaphore.acquire() 

511 

512 def acquire_nowait(self) -> None: 

513 self._semaphore.acquire_nowait() 

514 

515 def release(self) -> None: 

516 self._semaphore.release() 

517 

518 @property 

519 def value(self) -> int: 

520 if self._internal_semaphore is None: 

521 return self._initial_value 

522 

523 return self._semaphore.value 

524 

525 @property 

526 def max_value(self) -> int | None: 

527 return self._max_value 

528 

529 def statistics(self) -> SemaphoreStatistics: 

530 if self._internal_semaphore is None: 

531 return SemaphoreStatistics(tasks_waiting=0) 

532 

533 return self._semaphore.statistics() 

534 

535 

536class CapacityLimiter: 

537 __slots__ = ("__weakref__",) 

538 

539 def __new__(cls, total_tokens: float) -> CapacityLimiter: 

540 try: 

541 return get_async_backend().create_capacity_limiter(total_tokens) 

542 except NoEventLoopError: 

543 return CapacityLimiterAdapter(total_tokens) 

544 

545 async def __aenter__(self) -> None: 

546 raise NotImplementedError 

547 

548 async def __aexit__( 

549 self, 

550 exc_type: type[BaseException] | None, 

551 exc_val: BaseException | None, 

552 exc_tb: TracebackType | None, 

553 ) -> None: 

554 raise NotImplementedError 

555 

556 @property 

557 def total_tokens(self) -> float: 

558 """ 

559 The total number of tokens available for borrowing. 

560 

561 This is a read-write property. If the total number of tokens is increased, the 

562 proportionate number of tasks waiting on this limiter will be granted their 

563 tokens. 

564 

565 .. versionchanged:: 3.0 

566 The property is now writable. 

567 .. versionchanged:: 4.12 

568 The value can now be set to 0. 

569 

570 """ 

571 raise NotImplementedError 

572 

573 @total_tokens.setter 

574 def total_tokens(self, value: float) -> None: 

575 raise NotImplementedError 

576 

577 @property 

578 def borrowed_tokens(self) -> int: 

579 """The number of tokens that have currently been borrowed.""" 

580 raise NotImplementedError 

581 

582 @property 

583 def available_tokens(self) -> float: 

584 """The number of tokens currently available to be borrowed""" 

585 raise NotImplementedError 

586 

587 def acquire_nowait(self) -> None: 

588 """ 

589 Acquire a token for the current task without waiting for one to become 

590 available. 

591 

592 :raises ~anyio.WouldBlock: if there are no tokens available for borrowing 

593 

594 """ 

595 raise NotImplementedError 

596 

597 def acquire_on_behalf_of_nowait(self, borrower: object) -> None: 

598 """ 

599 Acquire a token without waiting for one to become available. 

600 

601 :param borrower: the entity borrowing a token 

602 :raises ~anyio.WouldBlock: if there are no tokens available for borrowing 

603 

604 """ 

605 raise NotImplementedError 

606 

607 async def acquire(self) -> None: 

608 """ 

609 Acquire a token for the current task, waiting if necessary for one to become 

610 available. 

611 

612 """ 

613 raise NotImplementedError 

614 

615 async def acquire_on_behalf_of(self, borrower: object) -> None: 

616 """ 

617 Acquire a token, waiting if necessary for one to become available. 

618 

619 :param borrower: the entity borrowing a token 

620 

621 """ 

622 raise NotImplementedError 

623 

624 def release(self) -> None: 

625 """ 

626 Release the token held by the current task. 

627 

628 :raises RuntimeError: if the current task has not borrowed a token from this 

629 limiter. 

630 

631 """ 

632 raise NotImplementedError 

633 

634 def release_on_behalf_of(self, borrower: object) -> None: 

635 """ 

636 Release the token held by the given borrower. 

637 

638 :raises RuntimeError: if the borrower has not borrowed a token from this 

639 limiter. 

640 

641 """ 

642 raise NotImplementedError 

643 

644 def statistics(self) -> CapacityLimiterStatistics: 

645 """ 

646 Return statistics about the current state of this limiter. 

647 

648 .. versionadded:: 3.0 

649 

650 """ 

651 raise NotImplementedError 

652 

653 

654class CapacityLimiterAdapter(CapacityLimiter): 

655 __slots__ = "_internal_limiter", "_total_tokens" 

656 

657 def __new__(cls, total_tokens: float) -> Self: 

658 return object.__new__(cls) 

659 

660 def __init__(self, total_tokens: float) -> None: 

661 self._internal_limiter: CapacityLimiter | None = None 

662 self.total_tokens = total_tokens 

663 

664 @property 

665 def _limiter(self) -> CapacityLimiter: 

666 if self._internal_limiter is None: 

667 self._internal_limiter = get_async_backend().create_capacity_limiter( 

668 self._total_tokens 

669 ) 

670 

671 return self._internal_limiter 

672 

673 async def __aenter__(self) -> None: 

674 await self._limiter.__aenter__() 

675 

676 async def __aexit__( 

677 self, 

678 exc_type: type[BaseException] | None, 

679 exc_val: BaseException | None, 

680 exc_tb: TracebackType | None, 

681 ) -> None: 

682 return await self._limiter.__aexit__(exc_type, exc_val, exc_tb) 

683 

684 @property 

685 def total_tokens(self) -> float: 

686 if self._internal_limiter is None: 

687 return self._total_tokens 

688 

689 return self._internal_limiter.total_tokens 

690 

691 @total_tokens.setter 

692 def total_tokens(self, value: float) -> None: 

693 if not isinstance(value, int) and not math.isinf(value): 

694 raise TypeError("total_tokens must be an int or math.inf") 

695 elif value < 0: 

696 raise ValueError("total_tokens must be >= 0") 

697 

698 if self._internal_limiter is None: 

699 self._total_tokens = value 

700 return 

701 

702 self._limiter.total_tokens = value 

703 

704 @property 

705 def borrowed_tokens(self) -> int: 

706 if self._internal_limiter is None: 

707 return 0 

708 

709 return self._internal_limiter.borrowed_tokens 

710 

711 @property 

712 def available_tokens(self) -> float: 

713 if self._internal_limiter is None: 

714 return self._total_tokens 

715 

716 return self._internal_limiter.available_tokens 

717 

718 def acquire_nowait(self) -> None: 

719 self._limiter.acquire_nowait() 

720 

721 def acquire_on_behalf_of_nowait(self, borrower: object) -> None: 

722 self._limiter.acquire_on_behalf_of_nowait(borrower) 

723 

724 async def acquire(self) -> None: 

725 await self._limiter.acquire() 

726 

727 async def acquire_on_behalf_of(self, borrower: object) -> None: 

728 await self._limiter.acquire_on_behalf_of(borrower) 

729 

730 def release(self) -> None: 

731 self._limiter.release() 

732 

733 def release_on_behalf_of(self, borrower: object) -> None: 

734 self._limiter.release_on_behalf_of(borrower) 

735 

736 def statistics(self) -> CapacityLimiterStatistics: 

737 if self._internal_limiter is None: 

738 return CapacityLimiterStatistics( 

739 borrowed_tokens=0, 

740 total_tokens=self.total_tokens, 

741 borrowers=(), 

742 tasks_waiting=0, 

743 ) 

744 

745 return self._internal_limiter.statistics() 

746 

747 

748class ResourceGuard: 

749 """ 

750 A context manager for ensuring that a resource is only used by a single task at a 

751 time. 

752 

753 Entering this context manager while the previous has not exited it yet will trigger 

754 :exc:`BusyResourceError`. 

755 

756 :param action: the action to guard against (visible in the :exc:`BusyResourceError` 

757 when triggered, e.g. "Another task is already {action} this resource") 

758 

759 .. versionadded:: 4.1 

760 """ 

761 

762 __slots__ = "__weakref__", "_guarded", "action" 

763 

764 def __init__(self, action: str = "using"): 

765 self.action: str = action 

766 self._guarded = False 

767 

768 def __enter__(self) -> None: 

769 if self._guarded: 

770 raise BusyResourceError(self.action) 

771 

772 self._guarded = True 

773 

774 def __exit__( 

775 self, 

776 exc_type: type[BaseException] | None, 

777 exc_val: BaseException | None, 

778 exc_tb: TracebackType | None, 

779 ) -> None: 

780 self._guarded = False