ez-a-sync 0.22.14__py3-none-any.whl → 0.22.16__py3-none-any.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 +37 -5
- a_sync/__init__.py +53 -12
- a_sync/_smart.py +231 -28
- a_sync/_typing.py +112 -15
- a_sync/a_sync/__init__.py +35 -10
- a_sync/a_sync/_descriptor.py +248 -38
- a_sync/a_sync/_flags.py +78 -9
- a_sync/a_sync/_helpers.py +46 -13
- a_sync/a_sync/_kwargs.py +33 -8
- a_sync/a_sync/_meta.py +149 -28
- a_sync/a_sync/abstract.py +150 -28
- a_sync/a_sync/base.py +34 -16
- a_sync/a_sync/config.py +85 -14
- a_sync/a_sync/decorator.py +441 -139
- a_sync/a_sync/function.py +709 -147
- a_sync/a_sync/method.py +437 -110
- a_sync/a_sync/modifiers/__init__.py +85 -5
- a_sync/a_sync/modifiers/cache/__init__.py +116 -17
- a_sync/a_sync/modifiers/cache/memory.py +130 -20
- a_sync/a_sync/modifiers/limiter.py +101 -22
- a_sync/a_sync/modifiers/manager.py +142 -16
- a_sync/a_sync/modifiers/semaphores.py +121 -15
- a_sync/a_sync/property.py +383 -82
- a_sync/a_sync/singleton.py +44 -19
- a_sync/aliases.py +0 -1
- a_sync/asyncio/__init__.py +140 -1
- a_sync/asyncio/as_completed.py +213 -79
- a_sync/asyncio/create_task.py +70 -20
- a_sync/asyncio/gather.py +125 -58
- a_sync/asyncio/utils.py +3 -3
- a_sync/exceptions.py +248 -26
- a_sync/executor.py +164 -69
- a_sync/future.py +1227 -168
- a_sync/iter.py +173 -56
- a_sync/primitives/__init__.py +14 -2
- a_sync/primitives/_debug.py +72 -18
- a_sync/primitives/_loggable.py +41 -10
- a_sync/primitives/locks/__init__.py +5 -2
- a_sync/primitives/locks/counter.py +107 -38
- a_sync/primitives/locks/event.py +21 -7
- a_sync/primitives/locks/prio_semaphore.py +262 -63
- a_sync/primitives/locks/semaphore.py +138 -89
- a_sync/primitives/queue.py +601 -60
- a_sync/sphinx/__init__.py +0 -1
- a_sync/sphinx/ext.py +160 -50
- a_sync/task.py +313 -112
- a_sync/utils/__init__.py +12 -6
- a_sync/utils/iterators.py +170 -50
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/METADATA +1 -1
- ez_a_sync-0.22.16.dist-info/RECORD +74 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -2
- tests/executor.py +250 -9
- tests/fixtures.py +61 -32
- tests/test_abstract.py +22 -4
- tests/test_as_completed.py +54 -21
- tests/test_base.py +264 -19
- tests/test_cache.py +31 -15
- tests/test_decorator.py +54 -28
- tests/test_executor.py +31 -13
- tests/test_future.py +45 -8
- tests/test_gather.py +8 -2
- tests/test_helpers.py +2 -0
- tests/test_iter.py +55 -13
- tests/test_limiter.py +5 -3
- tests/test_meta.py +23 -9
- tests/test_modified.py +4 -1
- tests/test_semaphore.py +15 -8
- tests/test_singleton.py +28 -11
- tests/test_task.py +162 -36
- ez_a_sync-0.22.14.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,8 @@
|
|
|
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
|
+
|
|
1
6
|
import asyncio
|
|
2
7
|
import functools
|
|
3
8
|
import logging
|
|
@@ -10,198 +15,242 @@ from a_sync.primitives._debug import _DebugDaemonMixin
|
|
|
10
15
|
|
|
11
16
|
logger = logging.getLogger(__name__)
|
|
12
17
|
|
|
18
|
+
|
|
13
19
|
class Semaphore(asyncio.Semaphore, _DebugDaemonMixin):
|
|
14
20
|
"""
|
|
15
|
-
A semaphore with additional debugging capabilities
|
|
16
|
-
|
|
17
|
-
This semaphore includes debug logging.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async with semaphore:
|
|
26
|
-
return 1
|
|
21
|
+
A semaphore with additional debugging capabilities inherited from :class:`_DebugDaemonMixin`.
|
|
22
|
+
|
|
23
|
+
This semaphore includes debug logging capabilities that are activated when the semaphore has waiters.
|
|
24
|
+
It allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
You can write this pattern:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
semaphore = Semaphore(5)
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
async def limited():
|
|
33
|
+
async with semaphore:
|
|
34
|
+
return 1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
like this:
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
```
|
|
40
|
+
semaphore = Semaphore(5)
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
@semaphore
|
|
43
|
+
async def limited():
|
|
44
|
+
return 1
|
|
45
|
+
```
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return 1
|
|
38
|
-
```
|
|
47
|
+
See Also:
|
|
48
|
+
:class:`_DebugDaemonMixin` for more details on debugging capabilities.
|
|
39
49
|
"""
|
|
50
|
+
|
|
40
51
|
if sys.version_info >= (3, 10):
|
|
41
52
|
__slots__ = "name", "_value", "_waiters", "_decorated"
|
|
42
53
|
else:
|
|
43
54
|
__slots__ = "name", "_value", "_waiters", "_loop", "_decorated"
|
|
44
|
-
|
|
55
|
+
|
|
45
56
|
def __init__(self, value: int, name=None, **kwargs) -> None:
|
|
46
57
|
"""
|
|
47
58
|
Initialize the semaphore with a given value and optional name for debugging.
|
|
48
|
-
|
|
59
|
+
|
|
49
60
|
Args:
|
|
50
61
|
value: The initial value for the semaphore.
|
|
51
62
|
name (optional): An optional name used only to provide useful context in debug logs.
|
|
52
63
|
"""
|
|
53
64
|
super().__init__(value, **kwargs)
|
|
54
|
-
self.name = name or self.__origin__ if hasattr(self,
|
|
65
|
+
self.name = name or self.__origin__ if hasattr(self, "__origin__") else None
|
|
55
66
|
self._decorated: Set[str] = set()
|
|
56
|
-
|
|
57
|
-
# Dank new functionality
|
|
58
67
|
|
|
59
68
|
def __call__(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
60
69
|
"""
|
|
61
|
-
|
|
70
|
+
Decorator method to wrap coroutine functions with the semaphore.
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
semaphore = Semaphore(5)
|
|
65
|
-
|
|
66
|
-
async def limited():
|
|
67
|
-
async with semaphore:
|
|
68
|
-
return 1
|
|
72
|
+
This allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
like this:
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
semaphore = Semaphore(5)
|
|
74
|
+
Example:
|
|
75
|
+
semaphore = Semaphore(5)
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```
|
|
77
|
+
@semaphore
|
|
78
|
+
async def limited():
|
|
79
|
+
return 1
|
|
81
80
|
"""
|
|
82
81
|
return self.decorate(fn) # type: ignore [arg-type, return-value]
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
def __repr__(self) -> str:
|
|
85
84
|
representation = f"<{self.__class__.__name__} name={self.name} value={self._value} waiters={len(self)}>"
|
|
86
85
|
if self._decorated:
|
|
87
86
|
representation = f"{representation[:-1]} decorates={self._decorated}"
|
|
88
87
|
return representation
|
|
89
|
-
|
|
88
|
+
|
|
90
89
|
def __len__(self) -> int:
|
|
91
90
|
return len(self._waiters) if self._waiters else 0
|
|
92
|
-
|
|
91
|
+
|
|
93
92
|
def decorate(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
94
93
|
"""
|
|
95
94
|
Wrap a coroutine function to ensure it runs with the semaphore.
|
|
96
|
-
|
|
97
|
-
Example:
|
|
98
|
-
Now you can rewrite this pattern:
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
semaphore = Semaphore(5)
|
|
102
|
-
|
|
103
|
-
async def limited():
|
|
104
|
-
async with semaphore:
|
|
105
|
-
return 1
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
like this:
|
|
110
|
-
|
|
111
|
-
```
|
|
96
|
+
Example:
|
|
112
97
|
semaphore = Semaphore(5)
|
|
113
98
|
|
|
114
99
|
@semaphore
|
|
115
100
|
async def limited():
|
|
116
101
|
return 1
|
|
117
|
-
```
|
|
118
102
|
"""
|
|
119
103
|
if not asyncio.iscoroutinefunction(fn):
|
|
120
104
|
raise TypeError(f"{fn} must be a coroutine function")
|
|
105
|
+
|
|
121
106
|
@functools.wraps(fn)
|
|
122
107
|
async def semaphore_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
123
108
|
async with self:
|
|
124
109
|
return await fn(*args, **kwargs)
|
|
110
|
+
|
|
125
111
|
self._decorated.add(f"{fn.__module__}.{fn.__name__}")
|
|
126
112
|
return semaphore_wrapper
|
|
127
113
|
|
|
128
114
|
async def acquire(self) -> Literal[True]:
|
|
115
|
+
"""
|
|
116
|
+
Acquire the semaphore, ensuring that debug logging is enabled if there are waiters.
|
|
117
|
+
|
|
118
|
+
If the semaphore value is zero or less, the debug daemon is started to log the state of the semaphore.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True when the semaphore is successfully acquired.
|
|
122
|
+
"""
|
|
129
123
|
if self._value <= 0:
|
|
130
124
|
self._ensure_debug_daemon()
|
|
131
125
|
return await super().acquire()
|
|
132
|
-
|
|
133
|
-
# Everything below just adds some debug logs
|
|
126
|
+
|
|
134
127
|
async def _debug_daemon(self) -> None:
|
|
135
128
|
"""
|
|
136
129
|
Daemon coroutine (runs in a background task) which will emit a debug log every minute while the semaphore has waiters.
|
|
130
|
+
|
|
131
|
+
This method is part of the :class:`_DebugDaemonMixin` and is used to provide detailed logging information
|
|
132
|
+
about the semaphore's state when it is being waited on.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
semaphore = Semaphore(5)
|
|
136
|
+
|
|
137
|
+
async def monitor():
|
|
138
|
+
await semaphore._debug_daemon()
|
|
137
139
|
"""
|
|
138
140
|
while self._waiters:
|
|
139
141
|
await asyncio.sleep(60)
|
|
140
|
-
self.logger.debug(
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
self.logger.debug(
|
|
143
|
+
f"{self} has {len(self)} waiters for any of: {self._decorated}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
143
147
|
class DummySemaphore(asyncio.Semaphore):
|
|
144
148
|
"""
|
|
145
149
|
A dummy semaphore that implements the standard :class:`asyncio.Semaphore` API but does nothing.
|
|
150
|
+
|
|
151
|
+
This class is useful for scenarios where a semaphore interface is required but no actual synchronization is needed.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
dummy_semaphore = DummySemaphore()
|
|
155
|
+
|
|
156
|
+
async def no_op():
|
|
157
|
+
async with dummy_semaphore:
|
|
158
|
+
return 1
|
|
146
159
|
"""
|
|
147
160
|
|
|
148
161
|
__slots__ = "name", "_value"
|
|
149
|
-
|
|
162
|
+
|
|
150
163
|
def __init__(self, name: Optional[str] = None):
|
|
164
|
+
"""
|
|
165
|
+
Initialize the dummy semaphore with an optional name.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
name (optional): An optional name for the dummy semaphore.
|
|
169
|
+
"""
|
|
151
170
|
self.name = name
|
|
152
171
|
self._value = 0
|
|
153
|
-
|
|
172
|
+
|
|
154
173
|
def __repr__(self) -> str:
|
|
155
174
|
return f"<{self.__class__.__name__} name={self.name}>"
|
|
156
|
-
|
|
175
|
+
|
|
157
176
|
async def acquire(self) -> Literal[True]:
|
|
158
177
|
return True
|
|
159
|
-
|
|
178
|
+
|
|
160
179
|
def release(self) -> None:
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
"""No-op release method."""
|
|
181
|
+
|
|
163
182
|
async def __aenter__(self):
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
"""No-op context manager entry."""
|
|
184
|
+
|
|
166
185
|
async def __aexit__(self, *args):
|
|
167
|
-
|
|
168
|
-
|
|
186
|
+
"""No-op context manager exit."""
|
|
187
|
+
|
|
169
188
|
|
|
170
189
|
class ThreadsafeSemaphore(Semaphore):
|
|
171
190
|
"""
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
A semaphore that works in a multi-threaded environment.
|
|
192
|
+
|
|
193
|
+
This semaphore ensures that the program functions correctly even when used with multiple event loops.
|
|
194
|
+
It provides a workaround for edge cases involving multiple threads and event loops by using a separate semaphore
|
|
195
|
+
for each thread.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
semaphore = ThreadsafeSemaphore(5)
|
|
199
|
+
|
|
200
|
+
async def limited():
|
|
201
|
+
async with semaphore:
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
See Also:
|
|
205
|
+
:class:`Semaphore` for the base class implementation.
|
|
177
206
|
"""
|
|
207
|
+
|
|
178
208
|
__slots__ = "semaphores", "dummy"
|
|
179
|
-
|
|
209
|
+
|
|
180
210
|
def __init__(self, value: Optional[int], name: Optional[str] = None) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Initialize the threadsafe semaphore with a given value and optional name.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
value: The initial value for the semaphore, should be an integer.
|
|
216
|
+
name (optional): An optional name for the semaphore.
|
|
217
|
+
"""
|
|
181
218
|
assert isinstance(value, int), f"{value} should be an integer."
|
|
182
219
|
super().__init__(value, name=name)
|
|
183
220
|
self.semaphores: DefaultDict[Thread, Semaphore] = defaultdict(lambda: Semaphore(value, name=self.name)) # type: ignore [arg-type]
|
|
184
221
|
self.dummy = DummySemaphore(name=name)
|
|
185
|
-
|
|
222
|
+
|
|
186
223
|
def __len__(self) -> int:
|
|
187
224
|
return sum(len(sem._waiters) for sem in self.semaphores.values())
|
|
188
|
-
|
|
225
|
+
|
|
189
226
|
@functools.cached_property
|
|
190
227
|
def use_dummy(self) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Determine whether to use a dummy semaphore.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if the semaphore value is None, indicating the use of a dummy semaphore.
|
|
233
|
+
"""
|
|
191
234
|
return self._value is None
|
|
192
|
-
|
|
235
|
+
|
|
193
236
|
@property
|
|
194
237
|
def semaphore(self) -> Semaphore:
|
|
195
238
|
"""
|
|
196
239
|
Returns the appropriate semaphore for the current thread.
|
|
197
|
-
|
|
240
|
+
|
|
198
241
|
NOTE: We can't cache this property because we need to check the current thread every time we access it.
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
semaphore = ThreadsafeSemaphore(5)
|
|
245
|
+
|
|
246
|
+
async def limited():
|
|
247
|
+
async with semaphore.semaphore:
|
|
248
|
+
return 1
|
|
199
249
|
"""
|
|
200
250
|
return self.dummy if self.use_dummy else self.semaphores[current_thread()]
|
|
201
|
-
|
|
251
|
+
|
|
202
252
|
async def __aenter__(self):
|
|
203
253
|
await self.semaphore.acquire()
|
|
204
|
-
|
|
254
|
+
|
|
205
255
|
async def __aexit__(self, *args):
|
|
206
256
|
self.semaphore.release()
|
|
207
|
-
|