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