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.

Files changed (73) hide show
  1. a_sync/ENVIRONMENT_VARIABLES.py +37 -5
  2. a_sync/__init__.py +53 -12
  3. a_sync/_smart.py +231 -28
  4. a_sync/_typing.py +112 -15
  5. a_sync/a_sync/__init__.py +35 -10
  6. a_sync/a_sync/_descriptor.py +248 -38
  7. a_sync/a_sync/_flags.py +78 -9
  8. a_sync/a_sync/_helpers.py +46 -13
  9. a_sync/a_sync/_kwargs.py +33 -8
  10. a_sync/a_sync/_meta.py +149 -28
  11. a_sync/a_sync/abstract.py +150 -28
  12. a_sync/a_sync/base.py +34 -16
  13. a_sync/a_sync/config.py +85 -14
  14. a_sync/a_sync/decorator.py +441 -139
  15. a_sync/a_sync/function.py +709 -147
  16. a_sync/a_sync/method.py +437 -110
  17. a_sync/a_sync/modifiers/__init__.py +85 -5
  18. a_sync/a_sync/modifiers/cache/__init__.py +116 -17
  19. a_sync/a_sync/modifiers/cache/memory.py +130 -20
  20. a_sync/a_sync/modifiers/limiter.py +101 -22
  21. a_sync/a_sync/modifiers/manager.py +142 -16
  22. a_sync/a_sync/modifiers/semaphores.py +121 -15
  23. a_sync/a_sync/property.py +383 -82
  24. a_sync/a_sync/singleton.py +44 -19
  25. a_sync/aliases.py +0 -1
  26. a_sync/asyncio/__init__.py +140 -1
  27. a_sync/asyncio/as_completed.py +213 -79
  28. a_sync/asyncio/create_task.py +70 -20
  29. a_sync/asyncio/gather.py +125 -58
  30. a_sync/asyncio/utils.py +3 -3
  31. a_sync/exceptions.py +248 -26
  32. a_sync/executor.py +164 -69
  33. a_sync/future.py +1227 -168
  34. a_sync/iter.py +173 -56
  35. a_sync/primitives/__init__.py +14 -2
  36. a_sync/primitives/_debug.py +72 -18
  37. a_sync/primitives/_loggable.py +41 -10
  38. a_sync/primitives/locks/__init__.py +5 -2
  39. a_sync/primitives/locks/counter.py +107 -38
  40. a_sync/primitives/locks/event.py +21 -7
  41. a_sync/primitives/locks/prio_semaphore.py +262 -63
  42. a_sync/primitives/locks/semaphore.py +138 -89
  43. a_sync/primitives/queue.py +601 -60
  44. a_sync/sphinx/__init__.py +0 -1
  45. a_sync/sphinx/ext.py +160 -50
  46. a_sync/task.py +313 -112
  47. a_sync/utils/__init__.py +12 -6
  48. a_sync/utils/iterators.py +170 -50
  49. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/METADATA +1 -1
  50. ez_a_sync-0.22.16.dist-info/RECORD +74 -0
  51. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/WHEEL +1 -1
  52. tests/conftest.py +1 -2
  53. tests/executor.py +250 -9
  54. tests/fixtures.py +61 -32
  55. tests/test_abstract.py +22 -4
  56. tests/test_as_completed.py +54 -21
  57. tests/test_base.py +264 -19
  58. tests/test_cache.py +31 -15
  59. tests/test_decorator.py +54 -28
  60. tests/test_executor.py +31 -13
  61. tests/test_future.py +45 -8
  62. tests/test_gather.py +8 -2
  63. tests/test_helpers.py +2 -0
  64. tests/test_iter.py +55 -13
  65. tests/test_limiter.py +5 -3
  66. tests/test_meta.py +23 -9
  67. tests/test_modified.py +4 -1
  68. tests/test_semaphore.py +15 -8
  69. tests/test_singleton.py +28 -11
  70. tests/test_task.py +162 -36
  71. ez_a_sync-0.22.14.dist-info/RECORD +0 -74
  72. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/LICENSE.txt +0 -0
  73. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/top_level.txt +0 -0
@@ -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
- Returns a logger instance specific to the class using this mixin.
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
- Returns:
23
- Logger: A logger instance for the class.
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, '_name') and self._name:
27
- logger_id += f'.{self._name}'
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
- Returns:
36
- bool: True if debug logging is enabled, False otherwise.
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 DummySemaphore, Semaphore, ThreadsafeSemaphore
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 manages :class:`asyncio.Task` objects that must wait for an internal counter to reach a specific value.
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 blocks until the internal counter has reached a specific value.
19
-
20
- A coroutine can `await counter.wait_for(3)` and it will block until the internal counter >= 3.
21
- If some other task executes `counter.value = 5` or `counter.set(5)`, the first coroutine will unblock as 5 >= 3.
22
-
23
- The internal counter can only increase.
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 (optional): An optional name for the counter, used in debug logs.
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
- Returns:
55
- True when the counter reaches or exceeds the specified value.
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 >= the current value.
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
- Returns:
84
- The current value of the counter.
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 = [self._events.pop(key) for key in list(self._events.keys()) if key <= self._value]
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("%s is still locked after %sm", self, round(time() - start / 60, 2))
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 2 or more CounterLock objects.
119
-
120
- `wait_for(i)` will block until the value of all CounterLock objects is >= i.
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
- __slots__ = "locks",
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
- Returns:
140
- True when the value of all CounterLock objects reach or exceed the specified value.
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(*[counter_lock.wait_for(value) for counter_lock in self.locks])
210
+ await asyncio.gather(
211
+ *[counter_lock.wait_for(value) for counter_lock in self.locks]
212
+ )
143
213
  return True
144
-
@@ -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
- def __init__(self, name: str = "", debug_daemon_interval: int = 300, *, loop: Optional[asyncio.AbstractEventLoop] = None):
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'name={self._name}' if self._name else 'object'
46
- status = 'set' if self._value else 'unset'
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', waiters:{len(self._waiters)}'
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("Waiting for %s for %sm", self, round((time() - start) / 60, 2))
83
+ self.logger.debug(
84
+ "Waiting for %s for %sm", self, round((time() - start) / 60, 2)
85
+ )