1from __future__ import annotations
2
3import math
4from collections import deque
5from collections.abc import Callable
6from dataclasses import dataclass
7from types import TracebackType
8from typing import TypeVar
9
10from ..lowlevel import checkpoint_if_cancelled
11from ._eventloop import get_async_backend
12from ._exceptions import BusyResourceError, NoEventLoopError
13from ._tasks import CancelScope
14from ._testing import TaskInfo, get_current_task
15
16T = TypeVar("T")
17
18
19@dataclass(frozen=True)
20class EventStatistics:
21 """
22 :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait`
23 """
24
25 tasks_waiting: int
26
27
28@dataclass(frozen=True)
29class CapacityLimiterStatistics:
30 """
31 :ivar int borrowed_tokens: number of tokens currently borrowed by tasks
32 :ivar float total_tokens: total number of available tokens
33 :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from
34 this limiter
35 :ivar int tasks_waiting: number of tasks waiting on
36 :meth:`~.CapacityLimiter.acquire` or
37 :meth:`~.CapacityLimiter.acquire_on_behalf_of`
38 """
39
40 borrowed_tokens: int
41 total_tokens: float
42 borrowers: tuple[object, ...]
43 tasks_waiting: int
44
45
46@dataclass(frozen=True)
47class LockStatistics:
48 """
49 :ivar bool locked: flag indicating if this lock is locked or not
50 :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the
51 lock is not held by any task)
52 :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire`
53 """
54
55 locked: bool
56 owner: TaskInfo | None
57 tasks_waiting: int
58
59
60@dataclass(frozen=True)
61class ConditionStatistics:
62 """
63 :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait`
64 :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying
65 :class:`~.Lock`
66 """
67
68 tasks_waiting: int
69 lock_statistics: LockStatistics
70
71
72@dataclass(frozen=True)
73class SemaphoreStatistics:
74 """
75 :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire`
76
77 """
78
79 tasks_waiting: int
80
81
82class Event:
83 __slots__ = ("__weakref__",)
84
85 def __new__(cls) -> Event:
86 try:
87 return get_async_backend().create_event()
88 except NoEventLoopError:
89 return EventAdapter()
90
91 def set(self) -> None:
92 """Set the flag, notifying all listeners."""
93 raise NotImplementedError
94
95 def is_set(self) -> bool:
96 """Return ``True`` if the flag is set, ``False`` if not."""
97 raise NotImplementedError
98
99 async def wait(self) -> None:
100 """
101 Wait until the flag has been set.
102
103 If the flag has already been set when this method is called, it returns
104 immediately.
105
106 """
107 raise NotImplementedError
108
109 def statistics(self) -> EventStatistics:
110 """Return statistics about the current state of this event."""
111 raise NotImplementedError
112
113
114class EventAdapter(Event):
115 __slots__ = "_internal_event", "_is_set"
116
117 def __new__(cls) -> EventAdapter:
118 return object.__new__(cls)
119
120 def __init__(self) -> None:
121 self._internal_event: Event | None = None
122 self._is_set = False
123
124 @property
125 def _event(self) -> Event:
126 if self._internal_event is None:
127 self._internal_event = get_async_backend().create_event()
128 if self._is_set:
129 self._internal_event.set()
130
131 return self._internal_event
132
133 def set(self) -> None:
134 if self._internal_event is None:
135 self._is_set = True
136 else:
137 self._event.set()
138
139 def is_set(self) -> bool:
140 if self._internal_event is None:
141 return self._is_set
142
143 return self._internal_event.is_set()
144
145 async def wait(self) -> None:
146 await self._event.wait()
147
148 def statistics(self) -> EventStatistics:
149 if self._internal_event is None:
150 return EventStatistics(tasks_waiting=0)
151
152 return self._internal_event.statistics()
153
154
155class Lock:
156 __slots__ = ("__weakref__",)
157
158 def __new__(cls, *, fast_acquire: bool = False) -> Lock:
159 try:
160 return get_async_backend().create_lock(fast_acquire=fast_acquire)
161 except NoEventLoopError:
162 return LockAdapter(fast_acquire=fast_acquire)
163
164 async def __aenter__(self) -> None:
165 await self.acquire()
166
167 async def __aexit__(
168 self,
169 exc_type: type[BaseException] | None,
170 exc_val: BaseException | None,
171 exc_tb: TracebackType | None,
172 ) -> None:
173 self.release()
174
175 async def acquire(self) -> None:
176 """Acquire the lock."""
177 raise NotImplementedError
178
179 def acquire_nowait(self) -> None:
180 """
181 Acquire the lock, without blocking.
182
183 :raises ~anyio.WouldBlock: if the operation would block
184
185 """
186 raise NotImplementedError
187
188 def release(self) -> None:
189 """Release the lock."""
190 raise NotImplementedError
191
192 def locked(self) -> bool:
193 """Return True if the lock is currently held."""
194 raise NotImplementedError
195
196 def statistics(self) -> LockStatistics:
197 """
198 Return statistics about the current state of this lock.
199
200 .. versionadded:: 3.0
201 """
202 raise NotImplementedError
203
204
205class LockAdapter(Lock):
206 __slots__ = "_internal_lock", "_fast_acquire"
207
208 def __new__(cls, *, fast_acquire: bool = False) -> LockAdapter:
209 return object.__new__(cls)
210
211 def __init__(self, *, fast_acquire: bool = False):
212 self._internal_lock: Lock | None = None
213 self._fast_acquire = fast_acquire
214
215 @property
216 def _lock(self) -> Lock:
217 if self._internal_lock is None:
218 self._internal_lock = get_async_backend().create_lock(
219 fast_acquire=self._fast_acquire
220 )
221
222 return self._internal_lock
223
224 async def __aenter__(self) -> None:
225 await self._lock.acquire()
226
227 async def __aexit__(
228 self,
229 exc_type: type[BaseException] | None,
230 exc_val: BaseException | None,
231 exc_tb: TracebackType | None,
232 ) -> None:
233 if self._internal_lock is not None:
234 self._internal_lock.release()
235
236 async def acquire(self) -> None:
237 """Acquire the lock."""
238 await self._lock.acquire()
239
240 def acquire_nowait(self) -> None:
241 """
242 Acquire the lock, without blocking.
243
244 :raises ~anyio.WouldBlock: if the operation would block
245
246 """
247 self._lock.acquire_nowait()
248
249 def release(self) -> None:
250 """Release the lock."""
251 self._lock.release()
252
253 def locked(self) -> bool:
254 """Return True if the lock is currently held."""
255 return self._lock.locked()
256
257 def statistics(self) -> LockStatistics:
258 """
259 Return statistics about the current state of this lock.
260
261 .. versionadded:: 3.0
262
263 """
264 if self._internal_lock is None:
265 return LockStatistics(False, None, 0)
266
267 return self._internal_lock.statistics()
268
269
270class Condition:
271 __slots__ = "__weakref__", "_owner_task", "_lock", "_waiters"
272
273 def __init__(self, lock: Lock | None = None):
274 self._owner_task: TaskInfo | None = None
275 self._lock = lock or Lock()
276 self._waiters: deque[Event] = deque()
277
278 async def __aenter__(self) -> None:
279 await self.acquire()
280
281 async def __aexit__(
282 self,
283 exc_type: type[BaseException] | None,
284 exc_val: BaseException | None,
285 exc_tb: TracebackType | None,
286 ) -> None:
287 self.release()
288
289 def _check_acquired(self) -> None:
290 if self._owner_task != get_current_task():
291 raise RuntimeError("The current task is not holding the underlying lock")
292
293 async def acquire(self) -> None:
294 """Acquire the underlying lock."""
295 await self._lock.acquire()
296 self._owner_task = get_current_task()
297
298 def acquire_nowait(self) -> None:
299 """
300 Acquire the underlying lock, without blocking.
301
302 :raises ~anyio.WouldBlock: if the operation would block
303
304 """
305 self._lock.acquire_nowait()
306 self._owner_task = get_current_task()
307
308 def release(self) -> None:
309 """Release the underlying lock."""
310 self._lock.release()
311
312 def locked(self) -> bool:
313 """Return True if the lock is set."""
314 return self._lock.locked()
315
316 def notify(self, n: int = 1) -> None:
317 """Notify exactly n listeners."""
318 self._check_acquired()
319 for _ in range(n):
320 try:
321 event = self._waiters.popleft()
322 except IndexError:
323 break
324
325 event.set()
326
327 def notify_all(self) -> None:
328 """Notify all the listeners."""
329 self._check_acquired()
330 for event in self._waiters:
331 event.set()
332
333 self._waiters.clear()
334
335 async def wait(self) -> None:
336 """Wait for a notification."""
337 await checkpoint_if_cancelled()
338 self._check_acquired()
339 event = Event()
340 self._waiters.append(event)
341 self.release()
342 try:
343 await event.wait()
344 except BaseException:
345 if not event.is_set():
346 self._waiters.remove(event)
347 elif self._waiters:
348 # This task was notified by could not act on it, so pass
349 # it on to the next task
350 self._waiters.popleft().set()
351
352 raise
353 finally:
354 with CancelScope(shield=True):
355 await self.acquire()
356
357 async def wait_for(self, predicate: Callable[[], T]) -> T:
358 """
359 Wait until a predicate becomes true.
360
361 :param predicate: a callable that returns a truthy value when the condition is
362 met
363 :return: the result of the predicate
364
365 .. versionadded:: 4.11.0
366
367 """
368 while not (result := predicate()):
369 await self.wait()
370
371 return result
372
373 def statistics(self) -> ConditionStatistics:
374 """
375 Return statistics about the current state of this condition.
376
377 .. versionadded:: 3.0
378 """
379 return ConditionStatistics(len(self._waiters), self._lock.statistics())
380
381
382class Semaphore:
383 __slots__ = "__weakref__", "_fast_acquire"
384
385 def __new__(
386 cls,
387 initial_value: int,
388 *,
389 max_value: int | None = None,
390 fast_acquire: bool = False,
391 ) -> Semaphore:
392 try:
393 return get_async_backend().create_semaphore(
394 initial_value, max_value=max_value, fast_acquire=fast_acquire
395 )
396 except NoEventLoopError:
397 return SemaphoreAdapter(initial_value, max_value=max_value)
398
399 def __init__(
400 self,
401 initial_value: int,
402 *,
403 max_value: int | None = None,
404 fast_acquire: bool = False,
405 ):
406 if not isinstance(initial_value, int):
407 raise TypeError("initial_value must be an integer")
408 if initial_value < 0:
409 raise ValueError("initial_value must be >= 0")
410 if max_value is not None:
411 if not isinstance(max_value, int):
412 raise TypeError("max_value must be an integer or None")
413 if max_value < initial_value:
414 raise ValueError(
415 "max_value must be equal to or higher than initial_value"
416 )
417
418 self._fast_acquire = fast_acquire
419
420 async def __aenter__(self) -> Semaphore:
421 await self.acquire()
422 return self
423
424 async def __aexit__(
425 self,
426 exc_type: type[BaseException] | None,
427 exc_val: BaseException | None,
428 exc_tb: TracebackType | None,
429 ) -> None:
430 self.release()
431
432 async def acquire(self) -> None:
433 """Decrement the semaphore value, blocking if necessary."""
434 raise NotImplementedError
435
436 def acquire_nowait(self) -> None:
437 """
438 Acquire the underlying lock, without blocking.
439
440 :raises ~anyio.WouldBlock: if the operation would block
441
442 """
443 raise NotImplementedError
444
445 def release(self) -> None:
446 """Increment the semaphore value."""
447 raise NotImplementedError
448
449 @property
450 def value(self) -> int:
451 """The current value of the semaphore."""
452 raise NotImplementedError
453
454 @property
455 def max_value(self) -> int | None:
456 """The maximum value of the semaphore."""
457 raise NotImplementedError
458
459 def statistics(self) -> SemaphoreStatistics:
460 """
461 Return statistics about the current state of this semaphore.
462
463 .. versionadded:: 3.0
464 """
465 raise NotImplementedError
466
467
468class SemaphoreAdapter(Semaphore):
469 __slots__ = "_internal_semaphore", "_initial_value", "_max_value"
470
471 def __new__(
472 cls,
473 initial_value: int,
474 *,
475 max_value: int | None = None,
476 fast_acquire: bool = False,
477 ) -> SemaphoreAdapter:
478 return object.__new__(cls)
479
480 def __init__(
481 self,
482 initial_value: int,
483 *,
484 max_value: int | None = None,
485 fast_acquire: bool = False,
486 ) -> None:
487 super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire)
488 self._internal_semaphore: Semaphore | None = None
489 self._initial_value = initial_value
490 self._max_value = max_value
491
492 @property
493 def _semaphore(self) -> Semaphore:
494 if self._internal_semaphore is None:
495 self._internal_semaphore = get_async_backend().create_semaphore(
496 self._initial_value, max_value=self._max_value
497 )
498
499 return self._internal_semaphore
500
501 async def acquire(self) -> None:
502 await self._semaphore.acquire()
503
504 def acquire_nowait(self) -> None:
505 self._semaphore.acquire_nowait()
506
507 def release(self) -> None:
508 self._semaphore.release()
509
510 @property
511 def value(self) -> int:
512 if self._internal_semaphore is None:
513 return self._initial_value
514
515 return self._semaphore.value
516
517 @property
518 def max_value(self) -> int | None:
519 return self._max_value
520
521 def statistics(self) -> SemaphoreStatistics:
522 if self._internal_semaphore is None:
523 return SemaphoreStatistics(tasks_waiting=0)
524
525 return self._semaphore.statistics()
526
527
528class CapacityLimiter:
529 __slots__ = ("__weakref__",)
530
531 def __new__(cls, total_tokens: float) -> CapacityLimiter:
532 try:
533 return get_async_backend().create_capacity_limiter(total_tokens)
534 except NoEventLoopError:
535 return CapacityLimiterAdapter(total_tokens)
536
537 async def __aenter__(self) -> None:
538 raise NotImplementedError
539
540 async def __aexit__(
541 self,
542 exc_type: type[BaseException] | None,
543 exc_val: BaseException | None,
544 exc_tb: TracebackType | None,
545 ) -> None:
546 raise NotImplementedError
547
548 @property
549 def total_tokens(self) -> float:
550 """
551 The total number of tokens available for borrowing.
552
553 This is a read-write property. If the total number of tokens is increased, the
554 proportionate number of tasks waiting on this limiter will be granted their
555 tokens.
556
557 .. versionchanged:: 3.0
558 The property is now writable.
559 .. versionchanged:: 4.12
560 The value can now be set to 0.
561
562 """
563 raise NotImplementedError
564
565 @total_tokens.setter
566 def total_tokens(self, value: float) -> None:
567 raise NotImplementedError
568
569 @property
570 def borrowed_tokens(self) -> int:
571 """The number of tokens that have currently been borrowed."""
572 raise NotImplementedError
573
574 @property
575 def available_tokens(self) -> float:
576 """The number of tokens currently available to be borrowed"""
577 raise NotImplementedError
578
579 def acquire_nowait(self) -> None:
580 """
581 Acquire a token for the current task without waiting for one to become
582 available.
583
584 :raises ~anyio.WouldBlock: if there are no tokens available for borrowing
585
586 """
587 raise NotImplementedError
588
589 def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
590 """
591 Acquire a token without waiting for one to become available.
592
593 :param borrower: the entity borrowing a token
594 :raises ~anyio.WouldBlock: if there are no tokens available for borrowing
595
596 """
597 raise NotImplementedError
598
599 async def acquire(self) -> None:
600 """
601 Acquire a token for the current task, waiting if necessary for one to become
602 available.
603
604 """
605 raise NotImplementedError
606
607 async def acquire_on_behalf_of(self, borrower: object) -> None:
608 """
609 Acquire a token, waiting if necessary for one to become available.
610
611 :param borrower: the entity borrowing a token
612
613 """
614 raise NotImplementedError
615
616 def release(self) -> None:
617 """
618 Release the token held by the current task.
619
620 :raises RuntimeError: if the current task has not borrowed a token from this
621 limiter.
622
623 """
624 raise NotImplementedError
625
626 def release_on_behalf_of(self, borrower: object) -> None:
627 """
628 Release the token held by the given borrower.
629
630 :raises RuntimeError: if the borrower has not borrowed a token from this
631 limiter.
632
633 """
634 raise NotImplementedError
635
636 def statistics(self) -> CapacityLimiterStatistics:
637 """
638 Return statistics about the current state of this limiter.
639
640 .. versionadded:: 3.0
641
642 """
643 raise NotImplementedError
644
645
646class CapacityLimiterAdapter(CapacityLimiter):
647 __slots__ = "_internal_limiter", "_total_tokens"
648
649 def __new__(cls, total_tokens: float) -> CapacityLimiterAdapter:
650 return object.__new__(cls)
651
652 def __init__(self, total_tokens: float) -> None:
653 self._internal_limiter: CapacityLimiter | None = None
654 self.total_tokens = total_tokens
655
656 @property
657 def _limiter(self) -> CapacityLimiter:
658 if self._internal_limiter is None:
659 self._internal_limiter = get_async_backend().create_capacity_limiter(
660 self._total_tokens
661 )
662
663 return self._internal_limiter
664
665 async def __aenter__(self) -> None:
666 await self._limiter.__aenter__()
667
668 async def __aexit__(
669 self,
670 exc_type: type[BaseException] | None,
671 exc_val: BaseException | None,
672 exc_tb: TracebackType | None,
673 ) -> None:
674 return await self._limiter.__aexit__(exc_type, exc_val, exc_tb)
675
676 @property
677 def total_tokens(self) -> float:
678 if self._internal_limiter is None:
679 return self._total_tokens
680
681 return self._internal_limiter.total_tokens
682
683 @total_tokens.setter
684 def total_tokens(self, value: float) -> None:
685 if not isinstance(value, int) and not math.isinf(value):
686 raise TypeError("total_tokens must be an int or math.inf")
687 elif value < 0:
688 raise ValueError("total_tokens must be >= 0")
689
690 if self._internal_limiter is None:
691 self._total_tokens = value
692 return
693
694 self._limiter.total_tokens = value
695
696 @property
697 def borrowed_tokens(self) -> int:
698 if self._internal_limiter is None:
699 return 0
700
701 return self._internal_limiter.borrowed_tokens
702
703 @property
704 def available_tokens(self) -> float:
705 if self._internal_limiter is None:
706 return self._total_tokens
707
708 return self._internal_limiter.available_tokens
709
710 def acquire_nowait(self) -> None:
711 self._limiter.acquire_nowait()
712
713 def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
714 self._limiter.acquire_on_behalf_of_nowait(borrower)
715
716 async def acquire(self) -> None:
717 await self._limiter.acquire()
718
719 async def acquire_on_behalf_of(self, borrower: object) -> None:
720 await self._limiter.acquire_on_behalf_of(borrower)
721
722 def release(self) -> None:
723 self._limiter.release()
724
725 def release_on_behalf_of(self, borrower: object) -> None:
726 self._limiter.release_on_behalf_of(borrower)
727
728 def statistics(self) -> CapacityLimiterStatistics:
729 if self._internal_limiter is None:
730 return CapacityLimiterStatistics(
731 borrowed_tokens=0,
732 total_tokens=self.total_tokens,
733 borrowers=(),
734 tasks_waiting=0,
735 )
736
737 return self._internal_limiter.statistics()
738
739
740class ResourceGuard:
741 """
742 A context manager for ensuring that a resource is only used by a single task at a
743 time.
744
745 Entering this context manager while the previous has not exited it yet will trigger
746 :exc:`BusyResourceError`.
747
748 :param action: the action to guard against (visible in the :exc:`BusyResourceError`
749 when triggered, e.g. "Another task is already {action} this resource")
750
751 .. versionadded:: 4.1
752 """
753
754 __slots__ = "__weakref__", "action", "_guarded"
755
756 def __init__(self, action: str = "using"):
757 self.action: str = action
758 self._guarded = False
759
760 def __enter__(self) -> None:
761 if self._guarded:
762 raise BusyResourceError(self.action)
763
764 self._guarded = True
765
766 def __exit__(
767 self,
768 exc_type: type[BaseException] | None,
769 exc_val: BaseException | None,
770 exc_tb: TracebackType | None,
771 ) -> None:
772 self._guarded = False