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

363 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 return self._lock.locked() 

262 

263 def statistics(self) -> LockStatistics: 

264 """ 

265 Return statistics about the current state of this lock. 

266 

267 .. versionadded:: 3.0 

268 

269 """ 

270 if self._internal_lock is None: 

271 return LockStatistics(False, None, 0) 

272 

273 return self._internal_lock.statistics() 

274 

275 

276class Condition: 

277 __slots__ = "__weakref__", "_lock", "_owner_task", "_waiters" 

278 

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

280 self._owner_task: TaskInfo | None = None 

281 self._lock = lock or Lock() 

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

283 

284 async def __aenter__(self) -> None: 

285 await self.acquire() 

286 

287 async def __aexit__( 

288 self, 

289 exc_type: type[BaseException] | None, 

290 exc_val: BaseException | None, 

291 exc_tb: TracebackType | None, 

292 ) -> None: 

293 self.release() 

294 

295 def _check_acquired(self) -> None: 

296 if self._owner_task != get_current_task(): 

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

298 

299 async def acquire(self) -> None: 

300 """Acquire the underlying lock.""" 

301 await self._lock.acquire() 

302 self._owner_task = get_current_task() 

303 

304 def acquire_nowait(self) -> None: 

305 """ 

306 Acquire the underlying lock, without blocking. 

307 

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

309 

310 """ 

311 self._lock.acquire_nowait() 

312 self._owner_task = get_current_task() 

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, max_value=self._max_value 

503 ) 

504 

505 return self._internal_semaphore 

506 

507 async def acquire(self) -> None: 

508 await self._semaphore.acquire() 

509 

510 def acquire_nowait(self) -> None: 

511 self._semaphore.acquire_nowait() 

512 

513 def release(self) -> None: 

514 self._semaphore.release() 

515 

516 @property 

517 def value(self) -> int: 

518 if self._internal_semaphore is None: 

519 return self._initial_value 

520 

521 return self._semaphore.value 

522 

523 @property 

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

525 return self._max_value 

526 

527 def statistics(self) -> SemaphoreStatistics: 

528 if self._internal_semaphore is None: 

529 return SemaphoreStatistics(tasks_waiting=0) 

530 

531 return self._semaphore.statistics() 

532 

533 

534class CapacityLimiter: 

535 __slots__ = ("__weakref__",) 

536 

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

538 try: 

539 return get_async_backend().create_capacity_limiter(total_tokens) 

540 except NoEventLoopError: 

541 return CapacityLimiterAdapter(total_tokens) 

542 

543 async def __aenter__(self) -> None: 

544 raise NotImplementedError 

545 

546 async def __aexit__( 

547 self, 

548 exc_type: type[BaseException] | None, 

549 exc_val: BaseException | None, 

550 exc_tb: TracebackType | None, 

551 ) -> None: 

552 raise NotImplementedError 

553 

554 @property 

555 def total_tokens(self) -> float: 

556 """ 

557 The total number of tokens available for borrowing. 

558 

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

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

561 tokens. 

562 

563 .. versionchanged:: 3.0 

564 The property is now writable. 

565 .. versionchanged:: 4.12 

566 The value can now be set to 0. 

567 

568 """ 

569 raise NotImplementedError 

570 

571 @total_tokens.setter 

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

573 raise NotImplementedError 

574 

575 @property 

576 def borrowed_tokens(self) -> int: 

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

578 raise NotImplementedError 

579 

580 @property 

581 def available_tokens(self) -> float: 

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

583 raise NotImplementedError 

584 

585 def acquire_nowait(self) -> None: 

586 """ 

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

588 available. 

589 

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

591 

592 """ 

593 raise NotImplementedError 

594 

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

596 """ 

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

598 

599 :param borrower: the entity borrowing a token 

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

601 

602 """ 

603 raise NotImplementedError 

604 

605 async def acquire(self) -> None: 

606 """ 

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

608 available. 

609 

610 """ 

611 raise NotImplementedError 

612 

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

614 """ 

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

616 

617 :param borrower: the entity borrowing a token 

618 

619 """ 

620 raise NotImplementedError 

621 

622 def release(self) -> None: 

623 """ 

624 Release the token held by the current task. 

625 

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

627 limiter. 

628 

629 """ 

630 raise NotImplementedError 

631 

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

633 """ 

634 Release the token held by the given borrower. 

635 

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

637 limiter. 

638 

639 """ 

640 raise NotImplementedError 

641 

642 def statistics(self) -> CapacityLimiterStatistics: 

643 """ 

644 Return statistics about the current state of this limiter. 

645 

646 .. versionadded:: 3.0 

647 

648 """ 

649 raise NotImplementedError 

650 

651 

652class CapacityLimiterAdapter(CapacityLimiter): 

653 __slots__ = "_internal_limiter", "_total_tokens" 

654 

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

656 return object.__new__(cls) 

657 

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

659 self._internal_limiter: CapacityLimiter | None = None 

660 self.total_tokens = total_tokens 

661 

662 @property 

663 def _limiter(self) -> CapacityLimiter: 

664 if self._internal_limiter is None: 

665 self._internal_limiter = get_async_backend().create_capacity_limiter( 

666 self._total_tokens 

667 ) 

668 

669 return self._internal_limiter 

670 

671 async def __aenter__(self) -> None: 

672 await self._limiter.__aenter__() 

673 

674 async def __aexit__( 

675 self, 

676 exc_type: type[BaseException] | None, 

677 exc_val: BaseException | None, 

678 exc_tb: TracebackType | None, 

679 ) -> None: 

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

681 

682 @property 

683 def total_tokens(self) -> float: 

684 if self._internal_limiter is None: 

685 return self._total_tokens 

686 

687 return self._internal_limiter.total_tokens 

688 

689 @total_tokens.setter 

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

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

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

693 elif value < 0: 

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

695 

696 if self._internal_limiter is None: 

697 self._total_tokens = value 

698 return 

699 

700 self._limiter.total_tokens = value 

701 

702 @property 

703 def borrowed_tokens(self) -> int: 

704 if self._internal_limiter is None: 

705 return 0 

706 

707 return self._internal_limiter.borrowed_tokens 

708 

709 @property 

710 def available_tokens(self) -> float: 

711 if self._internal_limiter is None: 

712 return self._total_tokens 

713 

714 return self._internal_limiter.available_tokens 

715 

716 def acquire_nowait(self) -> None: 

717 self._limiter.acquire_nowait() 

718 

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

720 self._limiter.acquire_on_behalf_of_nowait(borrower) 

721 

722 async def acquire(self) -> None: 

723 await self._limiter.acquire() 

724 

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

726 await self._limiter.acquire_on_behalf_of(borrower) 

727 

728 def release(self) -> None: 

729 self._limiter.release() 

730 

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

732 self._limiter.release_on_behalf_of(borrower) 

733 

734 def statistics(self) -> CapacityLimiterStatistics: 

735 if self._internal_limiter is None: 

736 return CapacityLimiterStatistics( 

737 borrowed_tokens=0, 

738 total_tokens=self.total_tokens, 

739 borrowers=(), 

740 tasks_waiting=0, 

741 ) 

742 

743 return self._internal_limiter.statistics() 

744 

745 

746class ResourceGuard: 

747 """ 

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

749 time. 

750 

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

752 :exc:`BusyResourceError`. 

753 

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

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

756 

757 .. versionadded:: 4.1 

758 """ 

759 

760 __slots__ = "__weakref__", "_guarded", "action" 

761 

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

763 self.action: str = action 

764 self._guarded = False 

765 

766 def __enter__(self) -> None: 

767 if self._guarded: 

768 raise BusyResourceError(self.action) 

769 

770 self._guarded = True 

771 

772 def __exit__( 

773 self, 

774 exc_type: type[BaseException] | None, 

775 exc_val: BaseException | None, 

776 exc_tb: TracebackType | None, 

777 ) -> None: 

778 self._guarded = False