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
a_sync/primitives/_loggable.py
CHANGED
|
@@ -9,22 +9,45 @@ from logging import Logger, getLogger, DEBUG
|
|
|
9
9
|
class _LoggerMixin:
|
|
10
10
|
"""
|
|
11
11
|
A mixin class that adds logging capabilities to other classes.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
This mixin provides a cached property for accessing a logger instance and a property to check if debug logging is enabled.
|
|
14
|
+
|
|
15
|
+
See Also:
|
|
16
|
+
- :func:`logging.getLogger`
|
|
17
|
+
- :class:`logging.Logger`
|
|
14
18
|
"""
|
|
19
|
+
|
|
15
20
|
@cached_property
|
|
16
21
|
def logger(self) -> Logger:
|
|
17
22
|
"""
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
Provides a logger instance specific to the class using this mixin.
|
|
24
|
+
|
|
20
25
|
The logger ID is constructed from the module and class name, and optionally includes an instance name if available.
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
Examples:
|
|
28
|
+
>>> class MyClass(_LoggerMixin):
|
|
29
|
+
... _name = "example"
|
|
30
|
+
...
|
|
31
|
+
>>> instance = MyClass()
|
|
32
|
+
>>> logger = instance.logger
|
|
33
|
+
>>> logger.name
|
|
34
|
+
'__main__.MyClass.example'
|
|
35
|
+
|
|
36
|
+
>>> class AnotherClass(_LoggerMixin):
|
|
37
|
+
... pass
|
|
38
|
+
...
|
|
39
|
+
>>> another_instance = AnotherClass()
|
|
40
|
+
>>> another_logger = another_instance.logger
|
|
41
|
+
>>> another_logger.name
|
|
42
|
+
'__main__.AnotherClass'
|
|
43
|
+
|
|
44
|
+
See Also:
|
|
45
|
+
- :func:`logging.getLogger`
|
|
46
|
+
- :class:`logging.Logger`
|
|
24
47
|
"""
|
|
25
|
-
logger_id = type(self).__qualname__
|
|
26
|
-
if hasattr(self,
|
|
27
|
-
logger_id += f
|
|
48
|
+
logger_id = f"{type(self).__module__}.{type(self).__qualname__}"
|
|
49
|
+
if hasattr(self, "_name") and self._name:
|
|
50
|
+
logger_id += f".{self._name}"
|
|
28
51
|
return getLogger(logger_id)
|
|
29
52
|
|
|
30
53
|
@property
|
|
@@ -32,7 +55,15 @@ class _LoggerMixin:
|
|
|
32
55
|
"""
|
|
33
56
|
Checks if debug logging is enabled for the logger.
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
Examples:
|
|
59
|
+
>>> class MyClass(_LoggerMixin):
|
|
60
|
+
... pass
|
|
61
|
+
...
|
|
62
|
+
>>> instance = MyClass()
|
|
63
|
+
>>> instance.debug_logs_enabled
|
|
64
|
+
False
|
|
65
|
+
|
|
66
|
+
See Also:
|
|
67
|
+
- :attr:`logging.Logger.isEnabledFor`
|
|
37
68
|
"""
|
|
38
69
|
return self.logger.isEnabledFor(DEBUG)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
1
|
from a_sync.primitives.locks.counter import CounterLock
|
|
3
2
|
from a_sync.primitives.locks.event import Event
|
|
4
|
-
from a_sync.primitives.locks.semaphore import
|
|
3
|
+
from a_sync.primitives.locks.semaphore import (
|
|
4
|
+
DummySemaphore,
|
|
5
|
+
Semaphore,
|
|
6
|
+
ThreadsafeSemaphore,
|
|
7
|
+
)
|
|
5
8
|
from a_sync.primitives.locks.prio_semaphore import PrioritySemaphore
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module provides two specialized async flow management classes, CounterLock and CounterLockCluster
|
|
2
|
+
This module provides two specialized async flow management classes, :class:`CounterLock` and :class:`CounterLockCluster`.
|
|
3
3
|
|
|
4
|
-
These primitives
|
|
4
|
+
These primitives manage :class:`asyncio.Task` objects that must wait for an internal counter to reach a specific value.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
@@ -15,23 +15,32 @@ from a_sync.primitives.locks.event import Event
|
|
|
15
15
|
|
|
16
16
|
class CounterLock(_DebugDaemonMixin):
|
|
17
17
|
"""
|
|
18
|
-
An async primitive that
|
|
19
|
-
|
|
20
|
-
A coroutine can `await counter.wait_for(3)` and it will
|
|
21
|
-
If some other task executes `counter.value = 5` or `counter.set(5)`, the first coroutine will
|
|
22
|
-
|
|
23
|
-
The internal counter can only
|
|
18
|
+
An async primitive that uses an internal counter to manage task synchronization.
|
|
19
|
+
|
|
20
|
+
A coroutine can `await counter.wait_for(3)` and it will wait until the internal counter >= 3.
|
|
21
|
+
If some other task executes `counter.value = 5` or `counter.set(5)`, the first coroutine will proceed as 5 >= 3.
|
|
22
|
+
|
|
23
|
+
The internal counter can only be set to a value greater than the current value.
|
|
24
|
+
|
|
25
|
+
See Also:
|
|
26
|
+
:class:`CounterLockCluster` for managing multiple :class:`CounterLock` instances.
|
|
24
27
|
"""
|
|
28
|
+
|
|
25
29
|
__slots__ = "is_ready", "_name", "_value", "_events"
|
|
30
|
+
|
|
26
31
|
def __init__(self, start_value: int = 0, name: Optional[str] = None):
|
|
27
32
|
"""
|
|
28
|
-
Initializes the CounterLock with a starting value and an optional name.
|
|
33
|
+
Initializes the :class:`CounterLock` with a starting value and an optional name.
|
|
29
34
|
|
|
30
35
|
Args:
|
|
31
36
|
start_value: The initial value of the counter.
|
|
32
|
-
name
|
|
33
|
-
"""
|
|
37
|
+
name: An optional name for the counter, used in debug logs.
|
|
34
38
|
|
|
39
|
+
Examples:
|
|
40
|
+
>>> counter = CounterLock(start_value=0, name="example_counter")
|
|
41
|
+
>>> counter.value
|
|
42
|
+
0
|
|
43
|
+
"""
|
|
35
44
|
self._name = name
|
|
36
45
|
"""An optional name for the counter, used in debug logs."""
|
|
37
46
|
|
|
@@ -43,7 +52,7 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
43
52
|
|
|
44
53
|
self.is_ready = lambda v: self._value >= v
|
|
45
54
|
"""A lambda function that indicates whether a given value has already been surpassed."""
|
|
46
|
-
|
|
55
|
+
|
|
47
56
|
async def wait_for(self, value: int) -> bool:
|
|
48
57
|
"""
|
|
49
58
|
Waits until the counter reaches or exceeds the specified value.
|
|
@@ -51,40 +60,67 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
51
60
|
Args:
|
|
52
61
|
value: The value to wait for.
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
Examples:
|
|
64
|
+
>>> counter = CounterLock(start_value=0)
|
|
65
|
+
>>> await counter.wait_for(5) # This will block until counter.value >= 5
|
|
66
|
+
|
|
67
|
+
See Also:
|
|
68
|
+
:meth:`CounterLock.set` to set the counter value.
|
|
56
69
|
"""
|
|
57
70
|
if not self.is_ready(value):
|
|
58
71
|
self._ensure_debug_daemon()
|
|
59
72
|
await self._events[value].wait()
|
|
60
73
|
return True
|
|
61
|
-
|
|
74
|
+
|
|
62
75
|
def set(self, value: int) -> None:
|
|
63
76
|
"""
|
|
64
77
|
Sets the counter to the specified value.
|
|
65
78
|
|
|
79
|
+
This method internally uses the `value` property to enforce that the new value must be strictly greater than the current value.
|
|
80
|
+
|
|
66
81
|
Args:
|
|
67
|
-
value: The value to set the counter to. Must be
|
|
68
|
-
|
|
82
|
+
value: The value to set the counter to. Must be strictly greater than the current value.
|
|
83
|
+
|
|
69
84
|
Raises:
|
|
70
|
-
ValueError: If the new value is less than the current value.
|
|
85
|
+
ValueError: If the new value is less than or equal to the current value.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
>>> counter = CounterLock(start_value=0)
|
|
89
|
+
>>> counter.set(5)
|
|
90
|
+
>>> counter.value
|
|
91
|
+
5
|
|
92
|
+
|
|
93
|
+
See Also:
|
|
94
|
+
:meth:`CounterLock.value` for direct value assignment.
|
|
71
95
|
"""
|
|
72
96
|
self.value = value
|
|
73
|
-
|
|
97
|
+
|
|
74
98
|
def __repr__(self) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Returns a string representation of the :class:`CounterLock` instance.
|
|
101
|
+
|
|
102
|
+
The representation includes the name, current value, and the number of waiters for each awaited value.
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
>>> counter = CounterLock(start_value=0, name="example_counter")
|
|
106
|
+
>>> repr(counter)
|
|
107
|
+
'<CounterLock name=example_counter value=0 waiters={}>'
|
|
108
|
+
"""
|
|
75
109
|
waiters = {v: len(self._events[v]._waiters) for v in sorted(self._events)}
|
|
76
110
|
return f"<CounterLock name={self._name} value={self._value} waiters={waiters}>"
|
|
77
|
-
|
|
111
|
+
|
|
78
112
|
@property
|
|
79
113
|
def value(self) -> int:
|
|
80
114
|
"""
|
|
81
115
|
Gets the current value of the counter.
|
|
82
116
|
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
Examples:
|
|
118
|
+
>>> counter = CounterLock(start_value=0)
|
|
119
|
+
>>> counter.value
|
|
120
|
+
0
|
|
85
121
|
"""
|
|
86
122
|
return self._value
|
|
87
|
-
|
|
123
|
+
|
|
88
124
|
@value.setter
|
|
89
125
|
def value(self, value: int) -> None:
|
|
90
126
|
"""
|
|
@@ -95,50 +131,83 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
95
131
|
|
|
96
132
|
Raises:
|
|
97
133
|
ValueError: If the new value is less than the current value.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
>>> counter = CounterLock(start_value=0)
|
|
137
|
+
>>> counter.value = 5
|
|
138
|
+
>>> counter.value
|
|
139
|
+
5
|
|
140
|
+
>>> counter.value = 3
|
|
141
|
+
Traceback (most recent call last):
|
|
142
|
+
...
|
|
143
|
+
ValueError: You cannot decrease the value.
|
|
98
144
|
"""
|
|
99
145
|
if value > self._value:
|
|
100
146
|
self._value = value
|
|
101
|
-
ready = [
|
|
147
|
+
ready = [
|
|
148
|
+
self._events.pop(key)
|
|
149
|
+
for key in list(self._events.keys())
|
|
150
|
+
if key <= self._value
|
|
151
|
+
]
|
|
102
152
|
for event in ready:
|
|
103
153
|
event.set()
|
|
104
154
|
elif value < self._value:
|
|
105
155
|
raise ValueError("You cannot decrease the value.")
|
|
106
|
-
|
|
156
|
+
|
|
107
157
|
async def _debug_daemon(self) -> None:
|
|
108
158
|
"""
|
|
109
159
|
Periodically logs debug information about the counter state and waiters.
|
|
160
|
+
|
|
161
|
+
This method is used internally to provide debugging information when debug logging is enabled.
|
|
110
162
|
"""
|
|
111
163
|
start = time()
|
|
112
164
|
while self._events:
|
|
113
|
-
self.logger.debug(
|
|
165
|
+
self.logger.debug(
|
|
166
|
+
"%s is still locked after %sm", self, round(time() - start / 60, 2)
|
|
167
|
+
)
|
|
114
168
|
await asyncio.sleep(300)
|
|
115
169
|
|
|
170
|
+
|
|
116
171
|
class CounterLockCluster:
|
|
117
172
|
"""
|
|
118
|
-
An asyncio primitive that represents
|
|
119
|
-
|
|
120
|
-
`wait_for(i)` will
|
|
173
|
+
An asyncio primitive that represents a collection of :class:`CounterLock` objects.
|
|
174
|
+
|
|
175
|
+
`wait_for(i)` will wait until the value of all :class:`CounterLock` objects is >= i.
|
|
176
|
+
|
|
177
|
+
See Also:
|
|
178
|
+
:class:`CounterLock` for managing individual counters.
|
|
121
179
|
"""
|
|
122
|
-
|
|
180
|
+
|
|
181
|
+
__slots__ = ("locks",)
|
|
182
|
+
|
|
123
183
|
def __init__(self, counter_locks: Iterable[CounterLock]) -> None:
|
|
124
184
|
"""
|
|
125
|
-
Initializes the CounterLockCluster with a collection of CounterLock objects.
|
|
185
|
+
Initializes the :class:`CounterLockCluster` with a collection of :class:`CounterLock` objects.
|
|
126
186
|
|
|
127
187
|
Args:
|
|
128
|
-
counter_locks: The CounterLock objects to manage.
|
|
188
|
+
counter_locks: The :class:`CounterLock` objects to manage.
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
>>> lock1 = CounterLock(start_value=0)
|
|
192
|
+
>>> lock2 = CounterLock(start_value=0)
|
|
193
|
+
>>> cluster = CounterLockCluster([lock1, lock2])
|
|
129
194
|
"""
|
|
130
195
|
self.locks = list(counter_locks)
|
|
131
|
-
|
|
196
|
+
|
|
132
197
|
async def wait_for(self, value: int) -> bool:
|
|
133
198
|
"""
|
|
134
|
-
Waits until the value of all CounterLock objects in the cluster reaches or exceeds the specified value.
|
|
199
|
+
Waits until the value of all :class:`CounterLock` objects in the cluster reaches or exceeds the specified value.
|
|
135
200
|
|
|
136
201
|
Args:
|
|
137
202
|
value: The value to wait for.
|
|
138
203
|
|
|
139
|
-
|
|
140
|
-
|
|
204
|
+
Examples:
|
|
205
|
+
>>> lock1 = CounterLock(start_value=0)
|
|
206
|
+
>>> lock2 = CounterLock(start_value=0)
|
|
207
|
+
>>> cluster = CounterLockCluster([lock1, lock2])
|
|
208
|
+
>>> await cluster.wait_for(5) # This will block until all locks have value >= 5
|
|
141
209
|
"""
|
|
142
|
-
await asyncio.gather(
|
|
210
|
+
await asyncio.gather(
|
|
211
|
+
*[counter_lock.wait_for(value) for counter_lock in self.locks]
|
|
212
|
+
)
|
|
143
213
|
return True
|
|
144
|
-
|
a_sync/primitives/locks/event.py
CHANGED
|
@@ -8,14 +8,16 @@ import sys
|
|
|
8
8
|
from a_sync._typing import *
|
|
9
9
|
from a_sync.primitives._debug import _DebugDaemonMixin
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class Event(asyncio.Event, _DebugDaemonMixin):
|
|
12
13
|
"""
|
|
13
14
|
An asyncio.Event with additional debug logging to help detect deadlocks.
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
This event class extends asyncio.Event by adding debug logging capabilities. It logs
|
|
16
|
-
detailed information about the event state and waiters, which can be useful for
|
|
17
|
+
detailed information about the event state and waiters, which can be useful for
|
|
17
18
|
diagnosing and debugging potential deadlocks.
|
|
18
19
|
"""
|
|
20
|
+
|
|
19
21
|
_value: bool
|
|
20
22
|
_loop: asyncio.AbstractEventLoop
|
|
21
23
|
_waiters: Deque["asyncio.Future[None]"]
|
|
@@ -23,7 +25,14 @@ class Event(asyncio.Event, _DebugDaemonMixin):
|
|
|
23
25
|
__slots__ = "_value", "_waiters", "_debug_daemon_interval"
|
|
24
26
|
else:
|
|
25
27
|
__slots__ = "_value", "_loop", "_waiters", "_debug_daemon_interval"
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
name: str = "",
|
|
32
|
+
debug_daemon_interval: int = 300,
|
|
33
|
+
*,
|
|
34
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
35
|
+
):
|
|
27
36
|
"""
|
|
28
37
|
Initializes the Event.
|
|
29
38
|
|
|
@@ -41,12 +50,14 @@ class Event(asyncio.Event, _DebugDaemonMixin):
|
|
|
41
50
|
if hasattr(self, "_loop"):
|
|
42
51
|
self._loop = self._loop or asyncio.get_event_loop()
|
|
43
52
|
self._debug_daemon_interval = debug_daemon_interval
|
|
53
|
+
|
|
44
54
|
def __repr__(self) -> str:
|
|
45
|
-
label = f
|
|
46
|
-
status =
|
|
55
|
+
label = f"name={self._name}" if self._name else "object"
|
|
56
|
+
status = "set" if self._value else "unset"
|
|
47
57
|
if self._waiters:
|
|
48
|
-
status += f
|
|
58
|
+
status += f", waiters:{len(self._waiters)}"
|
|
49
59
|
return f"<{self.__class__.__module__}.{self.__class__.__name__} {label} at {hex(id(self))} [{status}]>"
|
|
60
|
+
|
|
50
61
|
async def wait(self) -> Literal[True]:
|
|
51
62
|
"""
|
|
52
63
|
Wait until the event is set.
|
|
@@ -58,6 +69,7 @@ class Event(asyncio.Event, _DebugDaemonMixin):
|
|
|
58
69
|
return True
|
|
59
70
|
self._ensure_debug_daemon()
|
|
60
71
|
return await super().wait()
|
|
72
|
+
|
|
61
73
|
async def _debug_daemon(self) -> None:
|
|
62
74
|
"""
|
|
63
75
|
Periodically logs debug information about the event state and waiters.
|
|
@@ -68,4 +80,6 @@ class Event(asyncio.Event, _DebugDaemonMixin):
|
|
|
68
80
|
del self # no need to hold a reference here
|
|
69
81
|
await asyncio.sleep(self._debug_daemon_interval)
|
|
70
82
|
if (self := weakself()) and not self.is_set():
|
|
71
|
-
self.logger.debug(
|
|
83
|
+
self.logger.debug(
|
|
84
|
+
"Waiting for %s for %sm", self, round((time() - start) / 60, 2)
|
|
85
|
+
)
|