ez-a-sync 0.32.29__cp310-cp310-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ez-a-sync might be problematic. Click here for more details.
- a_sync/ENVIRONMENT_VARIABLES.py +42 -0
- a_sync/__init__.pxd +2 -0
- a_sync/__init__.py +145 -0
- a_sync/_smart.c +22803 -0
- a_sync/_smart.cp310-win32.pyd +0 -0
- a_sync/_smart.pxd +2 -0
- a_sync/_smart.pyi +202 -0
- a_sync/_smart.pyx +674 -0
- a_sync/_typing.py +258 -0
- a_sync/a_sync/__init__.py +60 -0
- a_sync/a_sync/_descriptor.c +20528 -0
- a_sync/a_sync/_descriptor.cp310-win32.pyd +0 -0
- a_sync/a_sync/_descriptor.pyi +33 -0
- a_sync/a_sync/_descriptor.pyx +422 -0
- a_sync/a_sync/_flags.c +6074 -0
- a_sync/a_sync/_flags.cp310-win32.pyd +0 -0
- a_sync/a_sync/_flags.pxd +3 -0
- a_sync/a_sync/_flags.pyx +92 -0
- a_sync/a_sync/_helpers.c +14521 -0
- a_sync/a_sync/_helpers.cp310-win32.pyd +0 -0
- a_sync/a_sync/_helpers.pxd +3 -0
- a_sync/a_sync/_helpers.pyi +10 -0
- a_sync/a_sync/_helpers.pyx +167 -0
- a_sync/a_sync/_kwargs.c +12194 -0
- a_sync/a_sync/_kwargs.cp310-win32.pyd +0 -0
- a_sync/a_sync/_kwargs.pxd +2 -0
- a_sync/a_sync/_kwargs.pyx +64 -0
- a_sync/a_sync/_meta.py +210 -0
- a_sync/a_sync/abstract.c +12411 -0
- a_sync/a_sync/abstract.cp310-win32.pyd +0 -0
- a_sync/a_sync/abstract.pyi +141 -0
- a_sync/a_sync/abstract.pyx +221 -0
- a_sync/a_sync/base.c +14932 -0
- a_sync/a_sync/base.cp310-win32.pyd +0 -0
- a_sync/a_sync/base.pyi +60 -0
- a_sync/a_sync/base.pyx +271 -0
- a_sync/a_sync/config.py +168 -0
- a_sync/a_sync/decorator.py +651 -0
- a_sync/a_sync/flags.c +5272 -0
- a_sync/a_sync/flags.cp310-win32.pyd +0 -0
- a_sync/a_sync/flags.pxd +72 -0
- a_sync/a_sync/flags.pyi +74 -0
- a_sync/a_sync/flags.pyx +72 -0
- a_sync/a_sync/function.c +37846 -0
- a_sync/a_sync/function.cp310-win32.pyd +0 -0
- a_sync/a_sync/function.pxd +28 -0
- a_sync/a_sync/function.pyi +571 -0
- a_sync/a_sync/function.pyx +1381 -0
- a_sync/a_sync/method.c +29774 -0
- a_sync/a_sync/method.cp310-win32.pyd +0 -0
- a_sync/a_sync/method.pxd +9 -0
- a_sync/a_sync/method.pyi +525 -0
- a_sync/a_sync/method.pyx +1023 -0
- a_sync/a_sync/modifiers/__init__.pxd +1 -0
- a_sync/a_sync/modifiers/__init__.py +101 -0
- a_sync/a_sync/modifiers/cache/__init__.py +160 -0
- a_sync/a_sync/modifiers/cache/memory.py +165 -0
- a_sync/a_sync/modifiers/limiter.py +132 -0
- a_sync/a_sync/modifiers/manager.c +16149 -0
- a_sync/a_sync/modifiers/manager.cp310-win32.pyd +0 -0
- a_sync/a_sync/modifiers/manager.pxd +5 -0
- a_sync/a_sync/modifiers/manager.pyi +219 -0
- a_sync/a_sync/modifiers/manager.pyx +299 -0
- a_sync/a_sync/modifiers/semaphores.py +173 -0
- a_sync/a_sync/property.c +27260 -0
- a_sync/a_sync/property.cp310-win32.pyd +0 -0
- a_sync/a_sync/property.pyi +376 -0
- a_sync/a_sync/property.pyx +819 -0
- a_sync/a_sync/singleton.py +63 -0
- a_sync/aliases.py +3 -0
- a_sync/async_property/__init__.pxd +1 -0
- a_sync/async_property/__init__.py +1 -0
- a_sync/async_property/cached.c +20386 -0
- a_sync/async_property/cached.cp310-win32.pyd +0 -0
- a_sync/async_property/cached.pxd +10 -0
- a_sync/async_property/cached.pyi +45 -0
- a_sync/async_property/cached.pyx +178 -0
- a_sync/async_property/proxy.c +34654 -0
- a_sync/async_property/proxy.cp310-win32.pyd +0 -0
- a_sync/async_property/proxy.pxd +2 -0
- a_sync/async_property/proxy.pyi +124 -0
- a_sync/async_property/proxy.pyx +474 -0
- a_sync/asyncio/__init__.pxd +6 -0
- a_sync/asyncio/__init__.py +164 -0
- a_sync/asyncio/as_completed.c +18841 -0
- a_sync/asyncio/as_completed.cp310-win32.pyd +0 -0
- a_sync/asyncio/as_completed.pxd +8 -0
- a_sync/asyncio/as_completed.pyi +109 -0
- a_sync/asyncio/as_completed.pyx +269 -0
- a_sync/asyncio/create_task.c +15902 -0
- a_sync/asyncio/create_task.cp310-win32.pyd +0 -0
- a_sync/asyncio/create_task.pxd +2 -0
- a_sync/asyncio/create_task.pyi +51 -0
- a_sync/asyncio/create_task.pyx +271 -0
- a_sync/asyncio/gather.c +16679 -0
- a_sync/asyncio/gather.cp310-win32.pyd +0 -0
- a_sync/asyncio/gather.pyi +107 -0
- a_sync/asyncio/gather.pyx +218 -0
- a_sync/asyncio/igather.c +12676 -0
- a_sync/asyncio/igather.cp310-win32.pyd +0 -0
- a_sync/asyncio/igather.pxd +1 -0
- a_sync/asyncio/igather.pyi +7 -0
- a_sync/asyncio/igather.pyx +182 -0
- a_sync/asyncio/sleep.c +9593 -0
- a_sync/asyncio/sleep.cp310-win32.pyd +0 -0
- a_sync/asyncio/sleep.pyi +14 -0
- a_sync/asyncio/sleep.pyx +49 -0
- a_sync/debugging.c +15362 -0
- a_sync/debugging.cp310-win32.pyd +0 -0
- a_sync/debugging.pyi +76 -0
- a_sync/debugging.pyx +107 -0
- a_sync/exceptions.c +13312 -0
- a_sync/exceptions.cp310-win32.pyd +0 -0
- a_sync/exceptions.pyi +376 -0
- a_sync/exceptions.pyx +446 -0
- a_sync/executor.py +619 -0
- a_sync/functools.c +12738 -0
- a_sync/functools.cp310-win32.pyd +0 -0
- a_sync/functools.pxd +7 -0
- a_sync/functools.pyi +33 -0
- a_sync/functools.pyx +139 -0
- a_sync/future.py +1497 -0
- a_sync/iter.c +37271 -0
- a_sync/iter.cp310-win32.pyd +0 -0
- a_sync/iter.pxd +11 -0
- a_sync/iter.pyi +370 -0
- a_sync/iter.pyx +981 -0
- a_sync/primitives/__init__.pxd +1 -0
- a_sync/primitives/__init__.py +53 -0
- a_sync/primitives/_debug.c +15757 -0
- a_sync/primitives/_debug.cp310-win32.pyd +0 -0
- a_sync/primitives/_debug.pxd +12 -0
- a_sync/primitives/_debug.pyi +52 -0
- a_sync/primitives/_debug.pyx +223 -0
- a_sync/primitives/_loggable.c +11529 -0
- a_sync/primitives/_loggable.cp310-win32.pyd +0 -0
- a_sync/primitives/_loggable.pxd +4 -0
- a_sync/primitives/_loggable.pyi +66 -0
- a_sync/primitives/_loggable.pyx +102 -0
- a_sync/primitives/locks/__init__.pxd +8 -0
- a_sync/primitives/locks/__init__.py +17 -0
- a_sync/primitives/locks/counter.c +17679 -0
- a_sync/primitives/locks/counter.cp310-win32.pyd +0 -0
- a_sync/primitives/locks/counter.pxd +12 -0
- a_sync/primitives/locks/counter.pyi +151 -0
- a_sync/primitives/locks/counter.pyx +260 -0
- a_sync/primitives/locks/event.c +17063 -0
- a_sync/primitives/locks/event.cp310-win32.pyd +0 -0
- a_sync/primitives/locks/event.pxd +22 -0
- a_sync/primitives/locks/event.pyi +43 -0
- a_sync/primitives/locks/event.pyx +185 -0
- a_sync/primitives/locks/prio_semaphore.c +25590 -0
- a_sync/primitives/locks/prio_semaphore.cp310-win32.pyd +0 -0
- a_sync/primitives/locks/prio_semaphore.pxd +25 -0
- a_sync/primitives/locks/prio_semaphore.pyi +217 -0
- a_sync/primitives/locks/prio_semaphore.pyx +597 -0
- a_sync/primitives/locks/semaphore.c +26509 -0
- a_sync/primitives/locks/semaphore.cp310-win32.pyd +0 -0
- a_sync/primitives/locks/semaphore.pxd +21 -0
- a_sync/primitives/locks/semaphore.pyi +197 -0
- a_sync/primitives/locks/semaphore.pyx +454 -0
- a_sync/primitives/queue.py +1022 -0
- a_sync/py.typed +0 -0
- a_sync/sphinx/__init__.py +3 -0
- a_sync/sphinx/ext.py +289 -0
- a_sync/task.py +932 -0
- a_sync/utils/__init__.py +105 -0
- a_sync/utils/iterators.py +297 -0
- a_sync/utils/repr.c +15799 -0
- a_sync/utils/repr.cp310-win32.pyd +0 -0
- a_sync/utils/repr.pyi +2 -0
- a_sync/utils/repr.pyx +73 -0
- ez_a_sync-0.32.29.dist-info/METADATA +367 -0
- ez_a_sync-0.32.29.dist-info/RECORD +177 -0
- ez_a_sync-0.32.29.dist-info/WHEEL +5 -0
- ez_a_sync-0.32.29.dist-info/licenses/LICENSE.txt +17 -0
- ez_a_sync-0.32.29.dist-info/top_level.txt +1 -0
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from a_sync.primitives._debug cimport _DebugDaemonMixin
|
|
2
|
+
|
|
3
|
+
cdef class Semaphore(_DebugDaemonMixin):
|
|
4
|
+
cdef unsigned long long __value
|
|
5
|
+
cdef object __waiters
|
|
6
|
+
cdef char* _name
|
|
7
|
+
cdef str decode_name(self)
|
|
8
|
+
cdef set _decorated
|
|
9
|
+
cdef dict __dict__
|
|
10
|
+
cpdef bint locked(self)
|
|
11
|
+
cpdef object acquire(self)
|
|
12
|
+
cpdef void release(self)
|
|
13
|
+
cpdef void _wake_up_next(self)
|
|
14
|
+
|
|
15
|
+
cdef class DummySemaphore(Semaphore):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
cdef class ThreadsafeSemaphore(Semaphore):
|
|
19
|
+
cdef object semaphores, dummy
|
|
20
|
+
cdef bint use_dummy
|
|
21
|
+
cdef Semaphore c_get_semaphore(self)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
from logging import Logger
|
|
4
|
+
from threading import Thread as Thread
|
|
5
|
+
from typing_extensions import Never
|
|
6
|
+
from a_sync._typing import *
|
|
7
|
+
from a_sync.primitives._debug import _DebugDaemonMixin
|
|
8
|
+
|
|
9
|
+
logger: Logger
|
|
10
|
+
|
|
11
|
+
class Semaphore(asyncio.Semaphore, _DebugDaemonMixin):
|
|
12
|
+
"""
|
|
13
|
+
A semaphore with additional debugging capabilities inherited from :class:`_DebugDaemonMixin`.
|
|
14
|
+
|
|
15
|
+
This semaphore includes debug logging capabilities that are activated when the semaphore has waiters.
|
|
16
|
+
It allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
You can write this pattern:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
semaphore = Semaphore(5)
|
|
23
|
+
|
|
24
|
+
async def limited():
|
|
25
|
+
async with semaphore:
|
|
26
|
+
return 1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
like this:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
semaphore = Semaphore(5)
|
|
33
|
+
|
|
34
|
+
@semaphore
|
|
35
|
+
async def limited():
|
|
36
|
+
return 1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See Also:
|
|
40
|
+
:class:`_DebugDaemonMixin` for more details on debugging capabilities.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
def __init__(self, value: int, name: str = "", **kwargs) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Initialize the semaphore with a given value and optional name for debugging.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
value: The initial value for the semaphore.
|
|
50
|
+
name (optional): An optional name used only to provide useful context in debug logs.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __call__(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
54
|
+
"""
|
|
55
|
+
Decorator method to wrap coroutine functions with the semaphore.
|
|
56
|
+
|
|
57
|
+
This allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
semaphore = Semaphore(5)
|
|
61
|
+
|
|
62
|
+
@semaphore
|
|
63
|
+
async def limited():
|
|
64
|
+
return 1
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __len__(self) -> int: ...
|
|
68
|
+
def decorate(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
69
|
+
"""
|
|
70
|
+
Wrap a coroutine function to ensure it runs with the semaphore.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
semaphore = Semaphore(5)
|
|
74
|
+
|
|
75
|
+
@semaphore
|
|
76
|
+
async def limited():
|
|
77
|
+
return 1
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
async def acquire(self) -> Literal[True]:
|
|
81
|
+
"""
|
|
82
|
+
Acquire the semaphore, ensuring that debug logging is enabled if there are waiters.
|
|
83
|
+
|
|
84
|
+
If the semaphore value is zero or less, the debug daemon is started to log the state of the semaphore.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True when the semaphore is successfully acquired.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
async def _debug_daemon(self) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Daemon coroutine (runs in a background task) which will emit a debug log every minute while the semaphore has waiters.
|
|
93
|
+
|
|
94
|
+
This method is part of the :class:`_DebugDaemonMixin` and is used to provide detailed logging information
|
|
95
|
+
about the semaphore's state when it is being waited on.
|
|
96
|
+
|
|
97
|
+
This code will only run if `self.logger.isEnabledFor(logging.DEBUG)` is True. You do not need to include any level checks in your custom implementations.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
semaphore = Semaphore(5)
|
|
101
|
+
|
|
102
|
+
async def monitor():
|
|
103
|
+
await semaphore._debug_daemon()
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
class DummySemaphore(asyncio.Semaphore):
|
|
107
|
+
"""
|
|
108
|
+
A dummy semaphore that implements the standard :class:`asyncio.Semaphore` API but does nothing.
|
|
109
|
+
|
|
110
|
+
This class is useful for scenarios where a semaphore interface is required but no actual synchronization is needed.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
dummy_semaphore = DummySemaphore()
|
|
114
|
+
|
|
115
|
+
async def no_op():
|
|
116
|
+
async with dummy_semaphore:
|
|
117
|
+
return 1
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
name: str
|
|
121
|
+
def __init__(self, name: Optional[str] = None) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Initialize the dummy semaphore with an optional name.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name (optional): An optional name for the dummy semaphore.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
async def acquire(self) -> Literal[True]:
|
|
130
|
+
"""Acquire the dummy semaphore, which is a no-op."""
|
|
131
|
+
|
|
132
|
+
def release(self) -> None:
|
|
133
|
+
"""No-op release method."""
|
|
134
|
+
|
|
135
|
+
async def __aenter__(self):
|
|
136
|
+
"""No-op context manager entry."""
|
|
137
|
+
|
|
138
|
+
async def __aexit__(self, *args) -> None:
|
|
139
|
+
"""No-op context manager exit."""
|
|
140
|
+
|
|
141
|
+
class ThreadsafeSemaphore(Semaphore):
|
|
142
|
+
"""
|
|
143
|
+
A semaphore that works in a multi-threaded environment.
|
|
144
|
+
|
|
145
|
+
This semaphore ensures that the program functions correctly even when used with multiple event loops.
|
|
146
|
+
It provides a workaround for edge cases involving multiple threads and event loops by using a separate semaphore
|
|
147
|
+
for each thread.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
semaphore = ThreadsafeSemaphore(5)
|
|
151
|
+
|
|
152
|
+
async def limited():
|
|
153
|
+
async with semaphore:
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
See Also:
|
|
157
|
+
:class:`Semaphore` for the base class implementation.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
semaphores: DefaultDict[Thread, Semaphore] | Dict[Never, Never]
|
|
161
|
+
dummy: DummySemaphore | None
|
|
162
|
+
def __init__(self, value: Optional[int], name: Optional[str] = None) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Initialize the threadsafe semaphore with a given value and optional name.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
value: The initial value for the semaphore, should be an integer.
|
|
168
|
+
name (optional): An optional name for the semaphore.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __len__(self) -> int: ...
|
|
172
|
+
@functools.cached_property
|
|
173
|
+
def use_dummy(self) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Determine whether to use a dummy semaphore.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if the semaphore value is None, indicating the use of a dummy semaphore.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def semaphore(self) -> Semaphore:
|
|
183
|
+
"""
|
|
184
|
+
Returns the appropriate semaphore for the current thread.
|
|
185
|
+
|
|
186
|
+
NOTE: We can't cache this property because we need to check the current thread every time we access it.
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
semaphore = ThreadsafeSemaphore(5)
|
|
190
|
+
|
|
191
|
+
async def limited():
|
|
192
|
+
async with semaphore.semaphore:
|
|
193
|
+
return 1
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
async def __aenter__(self) -> None: ...
|
|
197
|
+
async def __aexit__(self, *args) -> None: ...
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides various semaphore implementations, including a debug-enabled semaphore,
|
|
3
|
+
a dummy semaphore that does nothing, and a threadsafe semaphore for use in multi-threaded applications.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import collections
|
|
8
|
+
import threading
|
|
9
|
+
from libc.string cimport strcpy
|
|
10
|
+
from libc.stdlib cimport malloc, free
|
|
11
|
+
from typing import Container, Literal, List, Optional, Set
|
|
12
|
+
|
|
13
|
+
from cpython.unicode cimport PyUnicode_CompareWithASCIIString
|
|
14
|
+
|
|
15
|
+
from a_sync._typing import CoroFn, P, T
|
|
16
|
+
from a_sync.functools cimport wraps
|
|
17
|
+
from a_sync.primitives._debug cimport _DebugDaemonMixin, _LoopBoundMixin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# cdef asyncio
|
|
21
|
+
cdef object iscoroutinefunction = asyncio.iscoroutinefunction
|
|
22
|
+
cdef object sleep = asyncio.sleep
|
|
23
|
+
cdef object CancelledError = asyncio.CancelledError
|
|
24
|
+
cdef object Future = asyncio.Future
|
|
25
|
+
del asyncio
|
|
26
|
+
|
|
27
|
+
# cdef collections
|
|
28
|
+
cdef object defaultdict = collections.defaultdict
|
|
29
|
+
cdef object deque = collections.deque
|
|
30
|
+
del collections
|
|
31
|
+
|
|
32
|
+
# cdef threading
|
|
33
|
+
cdef object current_thread = threading.current_thread
|
|
34
|
+
cdef object Thread = threading.Thread
|
|
35
|
+
del threading
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _noop() -> Literal[True]:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
cdef class Semaphore(_DebugDaemonMixin):
|
|
43
|
+
"""
|
|
44
|
+
A semaphore with additional debugging capabilities inherited from :class:`_DebugDaemonMixin`.
|
|
45
|
+
|
|
46
|
+
This semaphore includes debug logging capabilities that are activated when the semaphore has waiters.
|
|
47
|
+
It allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
You can write this pattern:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
semaphore = Semaphore(5)
|
|
54
|
+
|
|
55
|
+
async def limited():
|
|
56
|
+
async with semaphore:
|
|
57
|
+
return 1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
like this:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
semaphore = Semaphore(5)
|
|
64
|
+
|
|
65
|
+
@semaphore
|
|
66
|
+
async def limited():
|
|
67
|
+
return 1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See Also:
|
|
71
|
+
:class:`_DebugDaemonMixin` for more details on debugging capabilities.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __cinit__(self) -> None:
|
|
75
|
+
self._Semaphore__waiters = deque()
|
|
76
|
+
self._decorated: Set[str] = set()
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
value: int = 1,
|
|
81
|
+
str name = "",
|
|
82
|
+
object loop = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Initialize the semaphore with a given value and optional name for debugging.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
value: The initial value for the semaphore.
|
|
89
|
+
name (optional): An optional name used only to provide useful context in debug logs.
|
|
90
|
+
"""
|
|
91
|
+
_LoopBoundMixin.__init__(self, loop=loop)
|
|
92
|
+
if value < 0:
|
|
93
|
+
raise ValueError("Semaphore initial value must be >= 0")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
self._Semaphore__value = value
|
|
97
|
+
except OverflowError as e:
|
|
98
|
+
if value < 0:
|
|
99
|
+
raise ValueError("'value' must be a positive integer") from e.__cause__
|
|
100
|
+
raise OverflowError(
|
|
101
|
+
{"error": str(e), "value": value, "max value": 18446744073709551615},
|
|
102
|
+
"If you need a Semaphore with a larger value, you should just use asyncio.Semaphore",
|
|
103
|
+
) from e.__cause__
|
|
104
|
+
|
|
105
|
+
# we need a constant to coerce to char*
|
|
106
|
+
cdef bytes encoded_name
|
|
107
|
+
try:
|
|
108
|
+
encoded_name = (name or getattr(self, "__origin__", "")).encode("utf-8")
|
|
109
|
+
except AttributeError:
|
|
110
|
+
# just a failsafe, please use string inputs
|
|
111
|
+
encoded_name = repr(name).encode("utf-8")
|
|
112
|
+
|
|
113
|
+
# Allocate memory for the char* and add 1 for the null character
|
|
114
|
+
cdef Py_ssize_t length = len(encoded_name)
|
|
115
|
+
self._name = <char*>malloc(length + 1)
|
|
116
|
+
"""An optional name for the counter, used in debug logs."""
|
|
117
|
+
|
|
118
|
+
if self._name == NULL:
|
|
119
|
+
raise MemoryError("Failed to allocate memory for __name.")
|
|
120
|
+
# Copy the bytes data into the char*
|
|
121
|
+
strcpy(self._name, encoded_name)
|
|
122
|
+
|
|
123
|
+
def __dealloc__(self):
|
|
124
|
+
# Free the memory allocated for _name
|
|
125
|
+
if self._name is not NULL:
|
|
126
|
+
free(self._name)
|
|
127
|
+
|
|
128
|
+
def __call__(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
129
|
+
"""
|
|
130
|
+
Decorator method to wrap coroutine functions with the semaphore.
|
|
131
|
+
|
|
132
|
+
This allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
semaphore = Semaphore(5)
|
|
136
|
+
|
|
137
|
+
@semaphore
|
|
138
|
+
async def limited():
|
|
139
|
+
return 1
|
|
140
|
+
"""
|
|
141
|
+
return self.decorate(fn) # type: ignore [arg-type, return-value]
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
representation = f"<{self.__class__.__name__} name={self.decode_name()} value={self._Semaphore__value} waiters={len(self)}>"
|
|
145
|
+
if self._decorated:
|
|
146
|
+
detail = next(iter(decorated)) if len(decorated := self._decorated) == 1 else decorated
|
|
147
|
+
representation = f"{representation[:-1]} decorates={detail}"
|
|
148
|
+
return representation
|
|
149
|
+
|
|
150
|
+
async def __aenter__(self):
|
|
151
|
+
await self.acquire()
|
|
152
|
+
# We have no use for the "as ..." clause in the with
|
|
153
|
+
# statement for locks.
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
157
|
+
self.release()
|
|
158
|
+
|
|
159
|
+
cpdef bint locked(self):
|
|
160
|
+
"""Returns True if semaphore cannot be acquired immediately."""
|
|
161
|
+
if self._Semaphore__value == 0:
|
|
162
|
+
return True
|
|
163
|
+
for w in self._Semaphore__waiters:
|
|
164
|
+
if _is_not_cancelled(w):
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
def __len__(self) -> int:
|
|
168
|
+
return len(self._Semaphore__waiters)
|
|
169
|
+
|
|
170
|
+
def decorate(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
171
|
+
"""
|
|
172
|
+
Wrap a coroutine function to ensure it runs with the semaphore.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
semaphore = Semaphore(5)
|
|
176
|
+
|
|
177
|
+
@semaphore
|
|
178
|
+
async def limited():
|
|
179
|
+
return 1
|
|
180
|
+
"""
|
|
181
|
+
if not iscoroutinefunction(fn):
|
|
182
|
+
raise TypeError(f"{fn} must be a coroutine function")
|
|
183
|
+
|
|
184
|
+
@wraps(fn)
|
|
185
|
+
async def semaphore_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
186
|
+
async with self:
|
|
187
|
+
return await fn(*args, **kwargs)
|
|
188
|
+
|
|
189
|
+
self._decorated.add(f"{fn.__module__}.{fn.__name__}")
|
|
190
|
+
return semaphore_wrapper
|
|
191
|
+
|
|
192
|
+
cpdef object acquire(self):
|
|
193
|
+
"""
|
|
194
|
+
Acquire the semaphore, ensuring that debug logging is enabled if there are waiters.
|
|
195
|
+
|
|
196
|
+
If the internal counter is larger than zero on entry, decrement it by one and return
|
|
197
|
+
True immediately. If it is zero on entry, block, waiting until some other coroutine
|
|
198
|
+
has called release() to make it larger than 0, and then return True.
|
|
199
|
+
|
|
200
|
+
If the semaphore value is zero or less, the debug daemon is started to log the state of the semaphore.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True when the semaphore is successfully acquired.
|
|
204
|
+
"""
|
|
205
|
+
if self._Semaphore__value <= 0:
|
|
206
|
+
self._c_ensure_debug_daemon((),{})
|
|
207
|
+
|
|
208
|
+
if not self.locked():
|
|
209
|
+
self._Semaphore__value -= 1
|
|
210
|
+
return _noop()
|
|
211
|
+
|
|
212
|
+
return self.__acquire()
|
|
213
|
+
|
|
214
|
+
async def __acquire(self) -> Literal[True]:
|
|
215
|
+
# Finally block should be called before the CancelledError
|
|
216
|
+
# handling as we don't want CancelledError to call
|
|
217
|
+
# _wake_up_first() and attempt to wake up itself.
|
|
218
|
+
|
|
219
|
+
cdef object fut = self._c_get_loop().create_future()
|
|
220
|
+
self._Semaphore__waiters.append(fut)
|
|
221
|
+
try:
|
|
222
|
+
try:
|
|
223
|
+
await fut
|
|
224
|
+
finally:
|
|
225
|
+
self._Semaphore__waiters.remove(fut)
|
|
226
|
+
except CancelledError:
|
|
227
|
+
if _is_not_cancelled(fut):
|
|
228
|
+
self._Semaphore__value += 1
|
|
229
|
+
self._wake_up_next()
|
|
230
|
+
raise
|
|
231
|
+
|
|
232
|
+
if self._Semaphore__value > 0:
|
|
233
|
+
self._wake_up_next()
|
|
234
|
+
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
cpdef void release(self):
|
|
238
|
+
"""Release a semaphore, incrementing the internal counter by one.
|
|
239
|
+
|
|
240
|
+
When it was zero on entry and another coroutine is waiting for it to
|
|
241
|
+
become larger than zero again, wake up that coroutine.
|
|
242
|
+
"""
|
|
243
|
+
self._Semaphore__value += 1
|
|
244
|
+
self._wake_up_next()
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def name(self) -> str:
|
|
248
|
+
return self.decode_name()
|
|
249
|
+
|
|
250
|
+
cdef str decode_name(self):
|
|
251
|
+
return (self._name or b"").decode("utf-8")
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def _value(self) -> int:
|
|
255
|
+
# required for subclass compatability
|
|
256
|
+
return self._Semaphore__value
|
|
257
|
+
|
|
258
|
+
@_value.setter
|
|
259
|
+
def _value(self, unsigned long long value):
|
|
260
|
+
# required for subclass compatability
|
|
261
|
+
self._Semaphore__value = value
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def _waiters(self) -> List[Future]:
|
|
265
|
+
# required for subclass compatability
|
|
266
|
+
return self._Semaphore__waiters
|
|
267
|
+
|
|
268
|
+
@_waiters.setter
|
|
269
|
+
def _waiters(self, value: Container):
|
|
270
|
+
# required for subclass compatability
|
|
271
|
+
self._Semaphore__waiters = value
|
|
272
|
+
|
|
273
|
+
cpdef void _wake_up_next(self):
|
|
274
|
+
"""Wake up the first waiter that isn't done."""
|
|
275
|
+
if not self._Semaphore__waiters:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
for fut in self._Semaphore__waiters:
|
|
279
|
+
if _is_not_done(fut):
|
|
280
|
+
self._Semaphore__value -= 1
|
|
281
|
+
fut.set_result(True)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
async def _debug_daemon(self) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Daemon coroutine (runs in a background task) which will emit a debug log every minute while the semaphore has waiters.
|
|
287
|
+
|
|
288
|
+
This method is part of the :class:`_DebugDaemonMixin` and is used to provide detailed logging information
|
|
289
|
+
about the semaphore's state when it is being waited on.
|
|
290
|
+
|
|
291
|
+
This code will only run if `self.logger.isEnabledFor(logging.DEBUG)` is True. You do not need to include any level checks in your custom implementations.
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
semaphore = Semaphore(5)
|
|
295
|
+
|
|
296
|
+
async def monitor():
|
|
297
|
+
await semaphore._debug_daemon()
|
|
298
|
+
"""
|
|
299
|
+
cdef object waiters = self._Semaphore__waiters
|
|
300
|
+
cdef set decorated = self._decorated
|
|
301
|
+
cdef object log = self.get_logger().debug
|
|
302
|
+
while waiters:
|
|
303
|
+
await sleep(60)
|
|
304
|
+
if waiters:
|
|
305
|
+
len_decorated = len(decorated)
|
|
306
|
+
if len_decorated == 0:
|
|
307
|
+
log("%s has %s waiters", self, len(self))
|
|
308
|
+
elif len_decorated == 1:
|
|
309
|
+
log(
|
|
310
|
+
"%s has %s waiters for %s",
|
|
311
|
+
self,
|
|
312
|
+
len(self),
|
|
313
|
+
next(iter(decorated)),
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
log(
|
|
317
|
+
"%s has %s waiters for any of: %s",
|
|
318
|
+
self,
|
|
319
|
+
len(self),
|
|
320
|
+
decorated,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
cdef inline bint _is_not_done(fut: Future):
|
|
325
|
+
return PyUnicode_CompareWithASCIIString(fut._state, b"PENDING") == 0
|
|
326
|
+
|
|
327
|
+
cdef inline bint _is_not_cancelled(fut: Future):
|
|
328
|
+
return PyUnicode_CompareWithASCIIString(fut._state, b"CANCELLED") != 0
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
cdef class DummySemaphore(Semaphore):
|
|
332
|
+
"""
|
|
333
|
+
A dummy semaphore that implements the standard :class:`asyncio.Semaphore` API but does nothing.
|
|
334
|
+
|
|
335
|
+
This class is useful for scenarios where a semaphore interface is required but no actual synchronization is needed.
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
dummy_semaphore = DummySemaphore()
|
|
339
|
+
|
|
340
|
+
async def no_op():
|
|
341
|
+
async with dummy_semaphore:
|
|
342
|
+
return 1
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def __cinit__(self):
|
|
346
|
+
self._Semaphore__value = 0
|
|
347
|
+
self._Semaphore__waiters = deque()
|
|
348
|
+
self._decorated = None
|
|
349
|
+
|
|
350
|
+
def __init__(self, name: Optional[str] = None):
|
|
351
|
+
"""
|
|
352
|
+
Initialize the dummy semaphore with an optional name.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
name (optional): An optional name for the dummy semaphore.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
# we need a constant to coerce to char*
|
|
359
|
+
cdef bytes encoded_name = (name or getattr(self, "__origin__", "")).encode("utf-8")
|
|
360
|
+
cdef Py_ssize_t length = len(encoded_name)
|
|
361
|
+
|
|
362
|
+
# Allocate memory for the char* and add 1 for the null character
|
|
363
|
+
self._name = <char*>malloc(length + 1)
|
|
364
|
+
"""An optional name for the counter, used in debug logs."""
|
|
365
|
+
|
|
366
|
+
if self._name == NULL:
|
|
367
|
+
raise MemoryError("Failed to allocate memory for _name.")
|
|
368
|
+
|
|
369
|
+
# Copy the bytes data into the char*
|
|
370
|
+
strcpy(self._name, encoded_name)
|
|
371
|
+
|
|
372
|
+
def __repr__(self) -> str:
|
|
373
|
+
return "<{} name={}>".format(self.__class__.__name__, self.decode_name())
|
|
374
|
+
|
|
375
|
+
async def acquire(self) -> Literal[True]:
|
|
376
|
+
"""Acquire the dummy semaphore, which is a no-op."""
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
cpdef void release(self):
|
|
380
|
+
"""No-op release method."""
|
|
381
|
+
|
|
382
|
+
async def __aenter__(self):
|
|
383
|
+
"""No-op context manager entry."""
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
async def __aexit__(self, *args):
|
|
387
|
+
"""No-op context manager exit."""
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
cdef class ThreadsafeSemaphore(Semaphore):
|
|
391
|
+
"""
|
|
392
|
+
A semaphore that works in a multi-threaded environment.
|
|
393
|
+
|
|
394
|
+
This semaphore ensures that the program functions correctly even when used with multiple event loops.
|
|
395
|
+
It provides a workaround for edge cases involving multiple threads and event loops by using a separate semaphore
|
|
396
|
+
for each thread.
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
semaphore = ThreadsafeSemaphore(5)
|
|
400
|
+
|
|
401
|
+
async def limited():
|
|
402
|
+
async with semaphore:
|
|
403
|
+
return 1
|
|
404
|
+
|
|
405
|
+
See Also:
|
|
406
|
+
:class:`Semaphore` for the base class implementation.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def __init__(self, value: Optional[int], name: Optional[str] = None) -> None:
|
|
410
|
+
"""
|
|
411
|
+
Initialize the threadsafe semaphore with a given value and optional name.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
value: The initial value for the semaphore, should be an integer.
|
|
415
|
+
name (optional): An optional name for the semaphore.
|
|
416
|
+
"""
|
|
417
|
+
assert isinstance(value, int), f"{value} should be an integer."
|
|
418
|
+
Semaphore.__init__(self, value, name=name)
|
|
419
|
+
|
|
420
|
+
self.use_dummy = value is -1
|
|
421
|
+
if self.use_dummy:
|
|
422
|
+
self.semaphores = {}
|
|
423
|
+
self.dummy = DummySemaphore(name=name)
|
|
424
|
+
else:
|
|
425
|
+
self.semaphores: DefaultDict[Thread, Semaphore] = defaultdict(lambda: Semaphore(value, name=name)) # type: ignore [arg-type]
|
|
426
|
+
self.dummy = None
|
|
427
|
+
|
|
428
|
+
def __len__(self) -> int:
|
|
429
|
+
cdef dict[object, Semaphore] semaphores = self.semaphores
|
|
430
|
+
return sum(map(len, semaphores.values()))
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def semaphore(self) -> Semaphore:
|
|
434
|
+
"""
|
|
435
|
+
Returns the appropriate semaphore for the current thread.
|
|
436
|
+
|
|
437
|
+
NOTE: We can't cache this property because we need to check the current thread every time we access it.
|
|
438
|
+
|
|
439
|
+
Example:
|
|
440
|
+
semaphore = ThreadsafeSemaphore(5)
|
|
441
|
+
|
|
442
|
+
async def limited():
|
|
443
|
+
async with semaphore.semaphore:
|
|
444
|
+
return 1
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
cdef Semaphore c_get_semaphore(self):
|
|
448
|
+
return self.dummy if self.use_dummy else self.semaphores[current_thread()]
|
|
449
|
+
|
|
450
|
+
async def __aenter__(self):
|
|
451
|
+
await self.c_get_semaphore().acquire()
|
|
452
|
+
|
|
453
|
+
async def __aexit__(self, *args):
|
|
454
|
+
self.c_get_semaphore().release()
|