ez-a-sync 0.22.15__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 +34 -3
- a_sync/__init__.py +32 -9
- a_sync/_smart.py +105 -6
- a_sync/_typing.py +56 -3
- a_sync/a_sync/_descriptor.py +174 -12
- a_sync/a_sync/_flags.py +64 -3
- a_sync/a_sync/_helpers.py +40 -8
- a_sync/a_sync/_kwargs.py +30 -6
- a_sync/a_sync/_meta.py +35 -6
- a_sync/a_sync/abstract.py +57 -9
- a_sync/a_sync/config.py +44 -7
- a_sync/a_sync/decorator.py +217 -37
- a_sync/a_sync/function.py +339 -47
- a_sync/a_sync/method.py +241 -52
- a_sync/a_sync/modifiers/__init__.py +39 -1
- a_sync/a_sync/modifiers/cache/__init__.py +75 -5
- a_sync/a_sync/modifiers/cache/memory.py +50 -6
- a_sync/a_sync/modifiers/limiter.py +55 -6
- a_sync/a_sync/modifiers/manager.py +46 -2
- a_sync/a_sync/modifiers/semaphores.py +84 -11
- a_sync/a_sync/singleton.py +43 -19
- a_sync/asyncio/__init__.py +137 -1
- a_sync/asyncio/as_completed.py +44 -38
- a_sync/asyncio/create_task.py +46 -10
- a_sync/asyncio/gather.py +72 -25
- a_sync/exceptions.py +178 -11
- a_sync/executor.py +51 -3
- a_sync/future.py +671 -29
- a_sync/iter.py +64 -7
- a_sync/primitives/_debug.py +59 -5
- a_sync/primitives/_loggable.py +36 -6
- a_sync/primitives/locks/counter.py +74 -7
- a_sync/primitives/locks/prio_semaphore.py +87 -8
- a_sync/primitives/locks/semaphore.py +68 -20
- a_sync/primitives/queue.py +65 -26
- a_sync/task.py +51 -15
- a_sync/utils/iterators.py +52 -16
- {ez_a_sync-0.22.15.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.15.dist-info → ez_a_sync-0.22.16.dist-info}/WHEEL +1 -1
- tests/executor.py +150 -12
- tests/test_abstract.py +15 -0
- tests/test_base.py +198 -2
- tests/test_executor.py +23 -0
- tests/test_singleton.py +13 -1
- tests/test_task.py +45 -17
- ez_a_sync-0.22.15.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.15.dist-info → ez_a_sync-0.22.16.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.15.dist-info → ez_a_sync-0.22.16.dist-info}/top_level.txt +0 -0
a_sync/iter.py
CHANGED
|
@@ -125,7 +125,19 @@ class ASyncIterable(_AwaitableAsyncIterableMixin[T], Iterable[T]):
|
|
|
125
125
|
|
|
126
126
|
This class allows objects to be iterated over using either a standard `for` loop or an `async for` loop, making it versatile in scenarios where the mode of iteration (synchronous or asynchronous) needs to be flexible or is determined at runtime.
|
|
127
127
|
|
|
128
|
-
The class achieves this by implementing both `__iter__` and `__aiter__` methods, enabling it to return appropriate iterator objects that can handle synchronous and asynchronous iteration, respectively.
|
|
128
|
+
The class achieves this by implementing both `__iter__` and `__aiter__` methods, enabling it to return appropriate iterator objects that can handle synchronous and asynchronous iteration, respectively. However, note that synchronous iteration relies on the :class:`ASyncIterator` class, which uses `asyncio.get_event_loop().run_until_complete` to fetch items. This can raise a `RuntimeError` if the event loop is already running, resulting in a :class:`SyncModeInAsyncContextError`.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> async_iterable = ASyncIterable(some_async_iterable)
|
|
132
|
+
>>> async for item in async_iterable:
|
|
133
|
+
... print(item)
|
|
134
|
+
>>> for item in async_iterable:
|
|
135
|
+
... print(item)
|
|
136
|
+
|
|
137
|
+
See Also:
|
|
138
|
+
- :class:`ASyncIterator`
|
|
139
|
+
- :class:`ASyncFilter`
|
|
140
|
+
- :class:`ASyncSorter`
|
|
129
141
|
"""
|
|
130
142
|
|
|
131
143
|
@classmethod
|
|
@@ -178,7 +190,20 @@ class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
|
178
190
|
|
|
179
191
|
By implementing both `__next__` and `__anext__` methods, ASyncIterator enables objects to be iterated using standard iteration protocols while internally managing the complexities of asynchronous iteration. This design simplifies the use of asynchronous iterables in environments or frameworks that are not inherently asynchronous, such as standard synchronous functions or older codebases being gradually migrated to asynchronous IO.
|
|
180
192
|
|
|
181
|
-
|
|
193
|
+
Note:
|
|
194
|
+
Synchronous iteration with `ASyncIterator` uses `asyncio.get_event_loop().run_until_complete`, which can raise a `RuntimeError` if the event loop is already running. In such cases, a :class:`SyncModeInAsyncContextError` is raised, indicating that synchronous iteration is not possible in an already running event loop.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> async_iterator = ASyncIterator(some_async_iterator)
|
|
198
|
+
>>> async for item in async_iterator:
|
|
199
|
+
... print(item)
|
|
200
|
+
>>> for item in async_iterator:
|
|
201
|
+
... print(item)
|
|
202
|
+
|
|
203
|
+
See Also:
|
|
204
|
+
- :class:`ASyncIterable`
|
|
205
|
+
- :class:`ASyncFilter`
|
|
206
|
+
- :class:`ASyncSorter`
|
|
182
207
|
"""
|
|
183
208
|
|
|
184
209
|
def __next__(self) -> T:
|
|
@@ -270,6 +295,18 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
270
295
|
The ASyncGeneratorFunction class supports dynamic binding to instances, enabling it to be used as a method on class instances. When accessed as a descriptor, it automatically handles the binding to the instance, thereby allowing the wrapped async generator function to be invoked with instance context ('self') automatically provided. This feature is invaluable for designing classes that need to expose asynchronous generators as part of their interface while maintaining the ease of use and calling semantics similar to regular methods.
|
|
271
296
|
|
|
272
297
|
By providing a unified interface to asynchronous generator functions, this class facilitates the creation of APIs that are flexible and easy to use in a wide range of asynchronous programming scenarios. It abstracts away the complexities involved in managing asynchronous generator lifecycles and invocation semantics, making it easier for developers to integrate asynchronous iteration patterns into their applications.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> async def my_async_gen():
|
|
301
|
+
... yield 1
|
|
302
|
+
... yield 2
|
|
303
|
+
>>> async_gen_func = ASyncGeneratorFunction(my_async_gen)
|
|
304
|
+
>>> for item in async_gen_func():
|
|
305
|
+
... print(item)
|
|
306
|
+
|
|
307
|
+
See Also:
|
|
308
|
+
- :class:`ASyncIterator`
|
|
309
|
+
- :class:`ASyncIterable`
|
|
273
310
|
"""
|
|
274
311
|
|
|
275
312
|
_cache_handle: asyncio.TimerHandle
|
|
@@ -310,9 +347,6 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
310
347
|
Args:
|
|
311
348
|
*args: Positional arguments for the function.
|
|
312
349
|
**kwargs: Keyword arguments for the function.
|
|
313
|
-
|
|
314
|
-
Returns:
|
|
315
|
-
An :class:`ASyncIterator` wrapping the :class:`AsyncIterator` returned from the wrapped function call.
|
|
316
350
|
"""
|
|
317
351
|
if self.__weakself__ is None:
|
|
318
352
|
return ASyncIterator(self.__wrapped__(*args, **kwargs))
|
|
@@ -393,7 +427,19 @@ class ASyncFilter(_ASyncView[T]):
|
|
|
393
427
|
|
|
394
428
|
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
395
429
|
iterate over items, applying the filter function to each item to determine if it should be
|
|
396
|
-
included in the result.
|
|
430
|
+
included in the result. The filter function can be either synchronous or asynchronous.
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
>>> async def is_even(x):
|
|
434
|
+
... return x % 2 == 0
|
|
435
|
+
>>> filtered_iterable = ASyncFilter(is_even, some_async_iterable)
|
|
436
|
+
>>> async for item in filtered_iterable:
|
|
437
|
+
... print(item)
|
|
438
|
+
|
|
439
|
+
See Also:
|
|
440
|
+
- :class:`ASyncIterable`
|
|
441
|
+
- :class:`ASyncIterator`
|
|
442
|
+
- :class:`ASyncSorter`
|
|
397
443
|
"""
|
|
398
444
|
|
|
399
445
|
def __repr__(self) -> str:
|
|
@@ -445,7 +491,18 @@ class ASyncSorter(_ASyncView[T]):
|
|
|
445
491
|
An async sorter class that sorts items of an async iterable based on a provided key function.
|
|
446
492
|
|
|
447
493
|
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
448
|
-
iterate over items, applying the key function to each item for sorting.
|
|
494
|
+
iterate over items, applying the key function to each item for sorting. The key function can be
|
|
495
|
+
either synchronous or asynchronous. Note that the ASyncSorter instance can only be consumed once.
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
>>> sorted_iterable = ASyncSorter(some_async_iterable, key=lambda x: x.value)
|
|
499
|
+
>>> async for item in sorted_iterable:
|
|
500
|
+
... print(item)
|
|
501
|
+
|
|
502
|
+
See Also:
|
|
503
|
+
- :class:`ASyncIterable`
|
|
504
|
+
- :class:`ASyncIterator`
|
|
505
|
+
- :class:`ASyncFilter`
|
|
449
506
|
"""
|
|
450
507
|
|
|
451
508
|
reversed: bool = False
|
a_sync/primitives/_debug.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module provides a mixin class used to
|
|
2
|
+
This module provides a mixin class used to facilitate the creation of debugging daemons in subclasses.
|
|
3
3
|
|
|
4
|
-
The mixin
|
|
4
|
+
The mixin provides a framework for managing a debug daemon task, which can be used to emit rich debug logs from subclass instances whenever debug logging is enabled. Subclasses must implement the specific logging behavior.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import abc
|
|
@@ -13,9 +13,12 @@ from a_sync.primitives._loggable import _LoggerMixin
|
|
|
13
13
|
|
|
14
14
|
class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
15
15
|
"""
|
|
16
|
-
A mixin class that provides debugging capabilities using a daemon task.
|
|
16
|
+
A mixin class that provides a framework for debugging capabilities using a daemon task.
|
|
17
17
|
|
|
18
|
-
This mixin
|
|
18
|
+
This mixin sets up the structure for managing a debug daemon task. Subclasses are responsible for implementing the specific behavior of the daemon, including any logging functionality.
|
|
19
|
+
|
|
20
|
+
See Also:
|
|
21
|
+
:class:`_LoggerMixin` for logging capabilities.
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
__slots__ = ("_daemon",)
|
|
@@ -25,39 +28,77 @@ class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
|
25
28
|
"""
|
|
26
29
|
Abstract method to define the debug daemon's behavior.
|
|
27
30
|
|
|
31
|
+
Subclasses must implement this method to specify what the debug daemon should do, including any logging or monitoring tasks.
|
|
32
|
+
|
|
28
33
|
Args:
|
|
29
34
|
fut: The future associated with the daemon.
|
|
30
35
|
fn: The function to be debugged.
|
|
31
36
|
*args: Positional arguments for the function.
|
|
32
37
|
**kwargs: Keyword arguments for the function.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
Implementing a simple debug daemon in a subclass:
|
|
41
|
+
|
|
42
|
+
.. code-block:: python
|
|
43
|
+
|
|
44
|
+
class MyDebugClass(_DebugDaemonMixin):
|
|
45
|
+
async def _debug_daemon(self, fut, fn, *args, **kwargs):
|
|
46
|
+
while not fut.done():
|
|
47
|
+
self.logger.debug("Debugging...")
|
|
48
|
+
await asyncio.sleep(1)
|
|
33
49
|
"""
|
|
34
50
|
|
|
35
51
|
def _start_debug_daemon(self, *args, **kwargs) -> "asyncio.Future[None]":
|
|
36
52
|
"""
|
|
37
53
|
Starts the debug daemon task if debug logging is enabled and the event loop is running.
|
|
38
54
|
|
|
55
|
+
This method checks if debug logging is enabled and if the event loop is running. If both conditions are met, it starts the debug daemon task.
|
|
56
|
+
|
|
39
57
|
Args:
|
|
40
58
|
*args: Positional arguments for the debug daemon.
|
|
41
59
|
**kwargs: Keyword arguments for the debug daemon.
|
|
42
60
|
|
|
43
61
|
Returns:
|
|
44
62
|
The debug daemon task as an asyncio.Task, or a dummy future if debug logs are not enabled or if the daemon cannot be created.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
Starting the debug daemon:
|
|
66
|
+
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
my_instance = MyDebugClass()
|
|
70
|
+
my_instance._start_debug_daemon()
|
|
71
|
+
|
|
72
|
+
See Also:
|
|
73
|
+
:meth:`_ensure_debug_daemon` for ensuring the daemon is running.
|
|
45
74
|
"""
|
|
46
75
|
if self.debug_logs_enabled and asyncio.get_event_loop().is_running():
|
|
47
76
|
return asyncio.create_task(self._debug_daemon(*args, **kwargs))
|
|
48
|
-
# else we return a blank Future since we shouldn't or can't create the daemon
|
|
49
77
|
return asyncio.get_event_loop().create_future()
|
|
50
78
|
|
|
51
79
|
def _ensure_debug_daemon(self, *args, **kwargs) -> "asyncio.Future[None]":
|
|
52
80
|
"""
|
|
53
81
|
Ensures that the debug daemon task is running.
|
|
54
82
|
|
|
83
|
+
This method checks if the debug daemon is already running and starts it if necessary. It will only start the daemon if it is not already running.
|
|
84
|
+
|
|
55
85
|
Args:
|
|
56
86
|
*args: Positional arguments for the debug daemon.
|
|
57
87
|
**kwargs: Keyword arguments for the debug daemon.
|
|
58
88
|
|
|
59
89
|
Returns:
|
|
60
90
|
Either the debug daemon task or a dummy future if debug logging is not enabled.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
Ensuring the debug daemon is running:
|
|
94
|
+
|
|
95
|
+
.. code-block:: python
|
|
96
|
+
|
|
97
|
+
my_instance = MyDebugClass()
|
|
98
|
+
my_instance._ensure_debug_daemon()
|
|
99
|
+
|
|
100
|
+
See Also:
|
|
101
|
+
:meth:`_start_debug_daemon` for starting the daemon.
|
|
61
102
|
"""
|
|
62
103
|
if not self.debug_logs_enabled:
|
|
63
104
|
self._daemon = asyncio.get_event_loop().create_future()
|
|
@@ -70,11 +111,24 @@ class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
|
70
111
|
"""
|
|
71
112
|
Stops the debug daemon task.
|
|
72
113
|
|
|
114
|
+
This method cancels the debug daemon task if it is running. Raises a ValueError if the task to be stopped is not the current daemon.
|
|
115
|
+
|
|
73
116
|
Args:
|
|
74
117
|
t (optional): The task to be stopped, if any.
|
|
75
118
|
|
|
76
119
|
Raises:
|
|
77
120
|
ValueError: If `t` is not the current daemon.
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
Stopping the debug daemon:
|
|
124
|
+
|
|
125
|
+
.. code-block:: python
|
|
126
|
+
|
|
127
|
+
my_instance = MyDebugClass()
|
|
128
|
+
my_instance._stop_debug_daemon()
|
|
129
|
+
|
|
130
|
+
See Also:
|
|
131
|
+
:meth:`_ensure_debug_daemon` for ensuring the daemon is running.
|
|
78
132
|
"""
|
|
79
133
|
if t and t != self._daemon:
|
|
80
134
|
raise ValueError(f"{t} is not {self._daemon}")
|
a_sync/primitives/_loggable.py
CHANGED
|
@@ -11,19 +11,41 @@ class _LoggerMixin:
|
|
|
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
|
"""
|
|
15
19
|
|
|
16
20
|
@cached_property
|
|
17
21
|
def logger(self) -> Logger:
|
|
18
22
|
"""
|
|
19
|
-
|
|
23
|
+
Provides a logger instance specific to the class using this mixin.
|
|
20
24
|
|
|
21
25
|
The logger ID is constructed from the module and class name, and optionally includes an instance name if available.
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
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`
|
|
25
47
|
"""
|
|
26
|
-
logger_id = type(self).__qualname__
|
|
48
|
+
logger_id = f"{type(self).__module__}.{type(self).__qualname__}"
|
|
27
49
|
if hasattr(self, "_name") and self._name:
|
|
28
50
|
logger_id += f".{self._name}"
|
|
29
51
|
return getLogger(logger_id)
|
|
@@ -33,7 +55,15 @@ class _LoggerMixin:
|
|
|
33
55
|
"""
|
|
34
56
|
Checks if debug logging is enabled for the logger.
|
|
35
57
|
|
|
36
|
-
|
|
37
|
-
|
|
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`
|
|
38
68
|
"""
|
|
39
69
|
return self.logger.isEnabledFor(DEBUG)
|
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
These primitives manage :class:`asyncio.Task` objects that must wait for an internal counter to reach a specific value.
|
|
5
5
|
"""
|
|
@@ -21,17 +21,25 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
21
21
|
If some other task executes `counter.value = 5` or `counter.set(5)`, the first coroutine will proceed as 5 >= 3.
|
|
22
22
|
|
|
23
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
|
"""
|
|
25
28
|
|
|
26
29
|
__slots__ = "is_ready", "_name", "_value", "_events"
|
|
27
30
|
|
|
28
31
|
def __init__(self, start_value: int = 0, name: Optional[str] = None):
|
|
29
32
|
"""
|
|
30
|
-
Initializes the CounterLock with a starting value and an optional name.
|
|
33
|
+
Initializes the :class:`CounterLock` with a starting value and an optional name.
|
|
31
34
|
|
|
32
35
|
Args:
|
|
33
36
|
start_value: The initial value of the counter.
|
|
34
37
|
name: An optional name for the counter, used in debug logs.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> counter = CounterLock(start_value=0, name="example_counter")
|
|
41
|
+
>>> counter.value
|
|
42
|
+
0
|
|
35
43
|
"""
|
|
36
44
|
self._name = name
|
|
37
45
|
"""An optional name for the counter, used in debug logs."""
|
|
@@ -51,6 +59,13 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
51
59
|
|
|
52
60
|
Args:
|
|
53
61
|
value: The value to wait for.
|
|
62
|
+
|
|
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.
|
|
54
69
|
"""
|
|
55
70
|
if not self.is_ready(value):
|
|
56
71
|
self._ensure_debug_daemon()
|
|
@@ -61,15 +76,36 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
61
76
|
"""
|
|
62
77
|
Sets the counter to the specified value.
|
|
63
78
|
|
|
79
|
+
This method internally uses the `value` property to enforce that the new value must be strictly greater than the current value.
|
|
80
|
+
|
|
64
81
|
Args:
|
|
65
82
|
value: The value to set the counter to. Must be strictly greater than the current value.
|
|
66
83
|
|
|
67
84
|
Raises:
|
|
68
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.
|
|
69
95
|
"""
|
|
70
96
|
self.value = value
|
|
71
97
|
|
|
72
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
|
+
"""
|
|
73
109
|
waiters = {v: len(self._events[v]._waiters) for v in sorted(self._events)}
|
|
74
110
|
return f"<CounterLock name={self._name} value={self._value} waiters={waiters}>"
|
|
75
111
|
|
|
@@ -77,6 +113,11 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
77
113
|
def value(self) -> int:
|
|
78
114
|
"""
|
|
79
115
|
Gets the current value of the counter.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
>>> counter = CounterLock(start_value=0)
|
|
119
|
+
>>> counter.value
|
|
120
|
+
0
|
|
80
121
|
"""
|
|
81
122
|
return self._value
|
|
82
123
|
|
|
@@ -90,6 +131,16 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
90
131
|
|
|
91
132
|
Raises:
|
|
92
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.
|
|
93
144
|
"""
|
|
94
145
|
if value > self._value:
|
|
95
146
|
self._value = value
|
|
@@ -106,6 +157,8 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
106
157
|
async def _debug_daemon(self) -> None:
|
|
107
158
|
"""
|
|
108
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.
|
|
109
162
|
"""
|
|
110
163
|
start = time()
|
|
111
164
|
while self._events:
|
|
@@ -117,28 +170,42 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
117
170
|
|
|
118
171
|
class CounterLockCluster:
|
|
119
172
|
"""
|
|
120
|
-
An asyncio primitive that represents a collection of CounterLock objects.
|
|
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.
|
|
121
176
|
|
|
122
|
-
|
|
177
|
+
See Also:
|
|
178
|
+
:class:`CounterLock` for managing individual counters.
|
|
123
179
|
"""
|
|
124
180
|
|
|
125
181
|
__slots__ = ("locks",)
|
|
126
182
|
|
|
127
183
|
def __init__(self, counter_locks: Iterable[CounterLock]) -> None:
|
|
128
184
|
"""
|
|
129
|
-
Initializes the CounterLockCluster with a collection of CounterLock objects.
|
|
185
|
+
Initializes the :class:`CounterLockCluster` with a collection of :class:`CounterLock` objects.
|
|
130
186
|
|
|
131
187
|
Args:
|
|
132
|
-
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])
|
|
133
194
|
"""
|
|
134
195
|
self.locks = list(counter_locks)
|
|
135
196
|
|
|
136
197
|
async def wait_for(self, value: int) -> bool:
|
|
137
198
|
"""
|
|
138
|
-
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.
|
|
139
200
|
|
|
140
201
|
Args:
|
|
141
202
|
value: The value to wait for.
|
|
203
|
+
|
|
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
|
|
142
209
|
"""
|
|
143
210
|
await asyncio.gather(
|
|
144
211
|
*[counter_lock.wait_for(value) for counter_lock in self.locks]
|
|
@@ -30,8 +30,13 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
30
30
|
A semaphore that allows prioritization of waiters.
|
|
31
31
|
|
|
32
32
|
This semaphore manages waiters with associated priorities, ensuring that waiters with higher
|
|
33
|
-
priorities are processed before those with lower priorities.
|
|
34
|
-
|
|
33
|
+
priorities are processed before those with lower priorities. Subclasses must define the
|
|
34
|
+
`_top_priority` property to specify the default top priority behavior.
|
|
35
|
+
|
|
36
|
+
The `_context_manager_class` property should return the class used for managing semaphore contexts.
|
|
37
|
+
|
|
38
|
+
See Also:
|
|
39
|
+
:class:`PrioritySemaphore` for an implementation using numeric priorities.
|
|
35
40
|
"""
|
|
36
41
|
|
|
37
42
|
name: Optional[str]
|
|
@@ -55,7 +60,13 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
55
60
|
|
|
56
61
|
@property
|
|
57
62
|
def _top_priority(self) -> PT:
|
|
58
|
-
|
|
63
|
+
"""Defines the top priority for the semaphore.
|
|
64
|
+
|
|
65
|
+
Subclasses must implement this property to specify the default top priority.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
NotImplementedError: If not implemented in a subclass.
|
|
69
|
+
"""
|
|
59
70
|
raise NotImplementedError
|
|
60
71
|
|
|
61
72
|
def __init__(self, value: int = 1, *, name: Optional[str] = None) -> None:
|
|
@@ -64,6 +75,9 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
64
75
|
Args:
|
|
65
76
|
value: The initial capacity of the semaphore.
|
|
66
77
|
name: An optional name for the semaphore, used for debugging.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
>>> semaphore = _AbstractPrioritySemaphore(5, name="test_semaphore")
|
|
67
81
|
"""
|
|
68
82
|
|
|
69
83
|
self._context_managers = {}
|
|
@@ -85,15 +99,36 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
85
99
|
return f"<{self.__class__.__name__} name={self.name} capacity={self._capacity} value={self._value} waiters={self._count_waiters()}>"
|
|
86
100
|
|
|
87
101
|
async def __aenter__(self) -> None:
|
|
88
|
-
"""Enters the semaphore context, acquiring it with the top priority.
|
|
102
|
+
"""Enters the semaphore context, acquiring it with the top priority.
|
|
103
|
+
|
|
104
|
+
This method is part of the asynchronous context management protocol.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
108
|
+
>>> async with semaphore:
|
|
109
|
+
... await do_stuff()
|
|
110
|
+
"""
|
|
89
111
|
await self[self._top_priority].acquire()
|
|
90
112
|
|
|
91
113
|
async def __aexit__(self, *_) -> None:
|
|
92
|
-
"""Exits the semaphore context, releasing it with the top priority.
|
|
114
|
+
"""Exits the semaphore context, releasing it with the top priority.
|
|
115
|
+
|
|
116
|
+
This method is part of the asynchronous context management protocol.
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
120
|
+
>>> async with semaphore:
|
|
121
|
+
... await do_stuff()
|
|
122
|
+
"""
|
|
93
123
|
self[self._top_priority].release()
|
|
94
124
|
|
|
95
125
|
async def acquire(self) -> Literal[True]:
|
|
96
|
-
"""Acquires the semaphore with the top priority.
|
|
126
|
+
"""Acquires the semaphore with the top priority.
|
|
127
|
+
|
|
128
|
+
Examples:
|
|
129
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
130
|
+
>>> await semaphore.acquire()
|
|
131
|
+
"""
|
|
97
132
|
return await self[self._top_priority].acquire()
|
|
98
133
|
|
|
99
134
|
def __getitem__(
|
|
@@ -106,6 +141,10 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
106
141
|
|
|
107
142
|
Returns:
|
|
108
143
|
The context manager associated with the given priority.
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
147
|
+
>>> context_manager = semaphore[priority]
|
|
109
148
|
"""
|
|
110
149
|
priority = self._top_priority if priority is None else priority
|
|
111
150
|
if priority not in self._context_managers:
|
|
@@ -121,6 +160,10 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
121
160
|
|
|
122
161
|
Returns:
|
|
123
162
|
True if the semaphore cannot be acquired immediately, False otherwise.
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
166
|
+
>>> semaphore.locked()
|
|
124
167
|
"""
|
|
125
168
|
return self._value == 0 or (
|
|
126
169
|
any(
|
|
@@ -134,6 +177,10 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
134
177
|
|
|
135
178
|
Returns:
|
|
136
179
|
A dictionary mapping each priority to the number of waiters.
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
183
|
+
>>> semaphore._count_waiters()
|
|
137
184
|
"""
|
|
138
185
|
return {
|
|
139
186
|
manager._priority: len(manager.waiters)
|
|
@@ -146,6 +193,12 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
|
|
|
146
193
|
This method handles the waking of waiters based on priority. It includes an emergency
|
|
147
194
|
procedure to handle potential lost waiters, ensuring that no waiter is left indefinitely
|
|
148
195
|
waiting.
|
|
196
|
+
|
|
197
|
+
The emergency procedure is a temporary measure to address potential issues with lost waiters.
|
|
198
|
+
|
|
199
|
+
Examples:
|
|
200
|
+
>>> semaphore = _AbstractPrioritySemaphore(5)
|
|
201
|
+
>>> semaphore._wake_up_next()
|
|
149
202
|
"""
|
|
150
203
|
while self._waiters:
|
|
151
204
|
manager = heapq.heappop(self._waiters)
|
|
@@ -230,6 +283,10 @@ class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
|
|
|
230
283
|
parent: The parent semaphore.
|
|
231
284
|
priority: The priority associated with this context manager.
|
|
232
285
|
name: An optional name for the context manager, used for debugging.
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
>>> parent_semaphore = _AbstractPrioritySemaphore(5)
|
|
289
|
+
>>> context_manager = _AbstractPrioritySemaphoreContextManager(parent_semaphore, priority=1)
|
|
233
290
|
"""
|
|
234
291
|
|
|
235
292
|
self._parent = parent
|
|
@@ -256,6 +313,14 @@ class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
|
|
|
256
313
|
|
|
257
314
|
Returns:
|
|
258
315
|
True if this context manager has a lower priority than the other, False otherwise.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
TypeError: If the other object is not of the same type.
|
|
319
|
+
|
|
320
|
+
Examples:
|
|
321
|
+
>>> cm1 = _AbstractPrioritySemaphoreContextManager(parent, priority=1)
|
|
322
|
+
>>> cm2 = _AbstractPrioritySemaphoreContextManager(parent, priority=2)
|
|
323
|
+
>>> cm1 < cm2
|
|
259
324
|
"""
|
|
260
325
|
if type(other) is not type(self):
|
|
261
326
|
raise TypeError(f"{other} is not type {self.__class__.__name__}")
|
|
@@ -281,6 +346,10 @@ class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
|
|
|
281
346
|
zero on entry, block, waiting until some other coroutine has
|
|
282
347
|
called release() to make it larger than 0, and then return
|
|
283
348
|
True.
|
|
349
|
+
|
|
350
|
+
Examples:
|
|
351
|
+
>>> context_manager = _AbstractPrioritySemaphoreContextManager(parent, priority=1)
|
|
352
|
+
>>> await context_manager.acquire()
|
|
284
353
|
"""
|
|
285
354
|
if self._parent._value <= 0:
|
|
286
355
|
self._ensure_debug_daemon()
|
|
@@ -300,7 +369,12 @@ class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
|
|
|
300
369
|
return True
|
|
301
370
|
|
|
302
371
|
def release(self) -> None:
|
|
303
|
-
"""Releases the semaphore for this context manager.
|
|
372
|
+
"""Releases the semaphore for this context manager.
|
|
373
|
+
|
|
374
|
+
Examples:
|
|
375
|
+
>>> context_manager = _AbstractPrioritySemaphoreContextManager(parent, priority=1)
|
|
376
|
+
>>> context_manager.release()
|
|
377
|
+
"""
|
|
304
378
|
self._parent.release()
|
|
305
379
|
|
|
306
380
|
|
|
@@ -315,7 +389,9 @@ class _PrioritySemaphoreContextManager(
|
|
|
315
389
|
class PrioritySemaphore(_AbstractPrioritySemaphore[Numeric, _PrioritySemaphoreContextManager]): # type: ignore [type-var]
|
|
316
390
|
"""Semaphore that uses numeric priorities for waiters.
|
|
317
391
|
|
|
318
|
-
|
|
392
|
+
This class extends :class:`_AbstractPrioritySemaphore` and provides a concrete implementation
|
|
393
|
+
using numeric priorities. The `_context_manager_class` is set to :class:`_PrioritySemaphoreContextManager`,
|
|
394
|
+
and the `_top_priority` is set to -1, which is the highest priority.
|
|
319
395
|
|
|
320
396
|
Examples:
|
|
321
397
|
The primary way to use this semaphore is by specifying a priority.
|
|
@@ -329,6 +405,9 @@ class PrioritySemaphore(_AbstractPrioritySemaphore[Numeric, _PrioritySemaphoreCo
|
|
|
329
405
|
>>> priority_semaphore = PrioritySemaphore(10)
|
|
330
406
|
>>> async with priority_semaphore:
|
|
331
407
|
... await do_stuff()
|
|
408
|
+
|
|
409
|
+
See Also:
|
|
410
|
+
:class:`_AbstractPrioritySemaphore` for the base class implementation.
|
|
332
411
|
"""
|
|
333
412
|
|
|
334
413
|
_context_manager_class = _PrioritySemaphoreContextManager
|