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.

Files changed (73) hide show
  1. a_sync/ENVIRONMENT_VARIABLES.py +4 -3
  2. a_sync/__init__.py +30 -12
  3. a_sync/_smart.py +132 -28
  4. a_sync/_typing.py +56 -12
  5. a_sync/a_sync/__init__.py +35 -10
  6. a_sync/a_sync/_descriptor.py +74 -26
  7. a_sync/a_sync/_flags.py +14 -6
  8. a_sync/a_sync/_helpers.py +8 -7
  9. a_sync/a_sync/_kwargs.py +3 -2
  10. a_sync/a_sync/_meta.py +120 -28
  11. a_sync/a_sync/abstract.py +102 -28
  12. a_sync/a_sync/base.py +34 -16
  13. a_sync/a_sync/config.py +47 -13
  14. a_sync/a_sync/decorator.py +239 -117
  15. a_sync/a_sync/function.py +416 -146
  16. a_sync/a_sync/method.py +197 -59
  17. a_sync/a_sync/modifiers/__init__.py +47 -5
  18. a_sync/a_sync/modifiers/cache/__init__.py +46 -17
  19. a_sync/a_sync/modifiers/cache/memory.py +86 -20
  20. a_sync/a_sync/modifiers/limiter.py +52 -22
  21. a_sync/a_sync/modifiers/manager.py +98 -16
  22. a_sync/a_sync/modifiers/semaphores.py +48 -15
  23. a_sync/a_sync/property.py +383 -82
  24. a_sync/a_sync/singleton.py +1 -0
  25. a_sync/aliases.py +0 -1
  26. a_sync/asyncio/__init__.py +4 -1
  27. a_sync/asyncio/as_completed.py +177 -49
  28. a_sync/asyncio/create_task.py +31 -17
  29. a_sync/asyncio/gather.py +72 -52
  30. a_sync/asyncio/utils.py +3 -3
  31. a_sync/exceptions.py +78 -23
  32. a_sync/executor.py +118 -71
  33. a_sync/future.py +575 -158
  34. a_sync/iter.py +110 -50
  35. a_sync/primitives/__init__.py +14 -2
  36. a_sync/primitives/_debug.py +13 -13
  37. a_sync/primitives/_loggable.py +5 -4
  38. a_sync/primitives/locks/__init__.py +5 -2
  39. a_sync/primitives/locks/counter.py +38 -36
  40. a_sync/primitives/locks/event.py +21 -7
  41. a_sync/primitives/locks/prio_semaphore.py +182 -62
  42. a_sync/primitives/locks/semaphore.py +78 -77
  43. a_sync/primitives/queue.py +560 -58
  44. a_sync/sphinx/__init__.py +0 -1
  45. a_sync/sphinx/ext.py +160 -50
  46. a_sync/task.py +262 -97
  47. a_sync/utils/__init__.py +12 -6
  48. a_sync/utils/iterators.py +127 -43
  49. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.dist-info}/METADATA +1 -1
  50. ez_a_sync-0.22.15.dist-info/RECORD +74 -0
  51. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.dist-info}/WHEEL +1 -1
  52. tests/conftest.py +1 -2
  53. tests/executor.py +112 -9
  54. tests/fixtures.py +61 -32
  55. tests/test_abstract.py +7 -4
  56. tests/test_as_completed.py +54 -21
  57. tests/test_base.py +66 -17
  58. tests/test_cache.py +31 -15
  59. tests/test_decorator.py +54 -28
  60. tests/test_executor.py +8 -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 +15 -10
  70. tests/test_task.py +126 -28
  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.15.dist-info}/LICENSE.txt +0 -0
  73. {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.dist-info}/top_level.txt +0 -0
@@ -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
+ )
@@ -1,3 +1,8 @@
1
+ """
2
+ This module provides priority-based semaphore implementations. These semaphores allow
3
+ waiters to be assigned priorities, ensuring that higher priority waiters are
4
+ processed before lower priority ones.
5
+ """
1
6
 
2
7
  import asyncio
3
8
  import heapq
@@ -12,84 +17,155 @@ logger = logging.getLogger(__name__)
12
17
 
13
18
 
14
19
  class Priority(Protocol):
15
- def __lt__(self, other) -> bool:
16
- ...
20
+ def __lt__(self, other) -> bool: ...
21
+
22
+
23
+ PT = TypeVar("PT", bound=Priority)
24
+
25
+ CM = TypeVar("CM", bound="_AbstractPrioritySemaphoreContextManager[Priority]")
17
26
 
18
- PT = TypeVar('PT', bound=Priority)
19
-
20
- CM = TypeVar('CM', bound="_AbstractPrioritySemaphoreContextManager[Priority]")
21
27
 
22
28
  class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
29
+ """
30
+ A semaphore that allows prioritization of waiters.
31
+
32
+ This semaphore manages waiters with associated priorities, ensuring that waiters with higher
33
+ priorities are processed before those with lower priorities. If no priority is specified,
34
+ the semaphore uses a default top priority.
35
+ """
36
+
23
37
  name: Optional[str]
24
38
  _value: int
25
39
  _waiters: List["_AbstractPrioritySemaphoreContextManager[PT]"] # type: ignore [assignment]
26
- __slots__ = "name", "_value", "_waiters", "_context_managers", "_capacity", "_potential_lost_waiters"
40
+ _context_managers: Dict[PT, "_AbstractPrioritySemaphoreContextManager[PT]"]
41
+ __slots__ = (
42
+ "name",
43
+ "_value",
44
+ "_waiters",
45
+ "_context_managers",
46
+ "_capacity",
47
+ "_potential_lost_waiters",
48
+ )
27
49
 
28
50
  @property
29
- def _context_manager_class(self) -> Type["_AbstractPrioritySemaphoreContextManager[PT]"]:
51
+ def _context_manager_class(
52
+ self,
53
+ ) -> Type["_AbstractPrioritySemaphoreContextManager[PT]"]:
30
54
  raise NotImplementedError
31
-
55
+
32
56
  @property
33
57
  def _top_priority(self) -> PT:
34
58
  # You can use this so you can set priorities with non numeric comparable values
35
59
  raise NotImplementedError
36
60
 
37
61
  def __init__(self, value: int = 1, *, name: Optional[str] = None) -> None:
38
- self._context_managers: Dict[PT, _AbstractPrioritySemaphoreContextManager[PT]] = {}
62
+ """Initializes the priority semaphore.
63
+
64
+ Args:
65
+ value: The initial capacity of the semaphore.
66
+ name: An optional name for the semaphore, used for debugging.
67
+ """
68
+
69
+ self._context_managers = {}
70
+ """A dictionary mapping priorities to their context managers."""
71
+
39
72
  self._capacity = value
73
+ """The initial capacity of the semaphore."""
74
+
40
75
  super().__init__(value, name=name)
41
76
  self._waiters = []
77
+ """A heap queue of context managers, sorted by priority."""
78
+
42
79
  # NOTE: This should (hopefully) be temporary
43
80
  self._potential_lost_waiters: List["asyncio.Future[None]"] = []
81
+ """A list of futures representing waiters that might have been lost."""
44
82
 
45
83
  def __repr__(self) -> str:
84
+ """Returns a string representation of the semaphore."""
46
85
  return f"<{self.__class__.__name__} name={self.name} capacity={self._capacity} value={self._value} waiters={self._count_waiters()}>"
47
86
 
48
87
  async def __aenter__(self) -> None:
88
+ """Enters the semaphore context, acquiring it with the top priority."""
49
89
  await self[self._top_priority].acquire()
50
90
 
51
91
  async def __aexit__(self, *_) -> None:
92
+ """Exits the semaphore context, releasing it with the top priority."""
52
93
  self[self._top_priority].release()
53
-
94
+
54
95
  async def acquire(self) -> Literal[True]:
96
+ """Acquires the semaphore with the top priority."""
55
97
  return await self[self._top_priority].acquire()
56
-
57
- def __getitem__(self, priority: Optional[PT]) -> "_AbstractPrioritySemaphoreContextManager[PT]":
98
+
99
+ def __getitem__(
100
+ self, priority: Optional[PT]
101
+ ) -> "_AbstractPrioritySemaphoreContextManager[PT]":
102
+ """Gets the context manager for a given priority.
103
+
104
+ Args:
105
+ priority: The priority for which to get the context manager. If None, uses the top priority.
106
+
107
+ Returns:
108
+ The context manager associated with the given priority.
109
+ """
58
110
  priority = self._top_priority if priority is None else priority
59
111
  if priority not in self._context_managers:
60
- context_manager = self._context_manager_class(self, priority, name=self.name)
112
+ context_manager = self._context_manager_class(
113
+ self, priority, name=self.name
114
+ )
61
115
  heapq.heappush(self._waiters, context_manager) # type: ignore [misc]
62
116
  self._context_managers[priority] = context_manager
63
117
  return self._context_managers[priority]
64
118
 
65
119
  def locked(self) -> bool:
66
- """Returns True if semaphore cannot be acquired immediately."""
120
+ """Checks if the semaphore is locked.
121
+
122
+ Returns:
123
+ True if the semaphore cannot be acquired immediately, False otherwise.
124
+ """
67
125
  return self._value == 0 or (
68
126
  any(
69
- cm._waiters and any(not w.cancelled() for w in cm._waiters)
127
+ cm._waiters and any(not w.cancelled() for w in cm._waiters)
70
128
  for cm in (self._context_managers.values() or ())
71
129
  )
72
130
  )
73
-
131
+
74
132
  def _count_waiters(self) -> Dict[PT, int]:
75
- return {manager._priority: len(manager.waiters) for manager in sorted(self._waiters, key=lambda m: m._priority)}
76
-
133
+ """Counts the number of waiters for each priority.
134
+
135
+ Returns:
136
+ A dictionary mapping each priority to the number of waiters.
137
+ """
138
+ return {
139
+ manager._priority: len(manager.waiters)
140
+ for manager in sorted(self._waiters, key=lambda m: m._priority)
141
+ }
142
+
77
143
  def _wake_up_next(self) -> None:
144
+ """Wakes up the next waiter in line.
145
+
146
+ This method handles the waking of waiters based on priority. It includes an emergency
147
+ procedure to handle potential lost waiters, ensuring that no waiter is left indefinitely
148
+ waiting.
149
+ """
78
150
  while self._waiters:
79
151
  manager = heapq.heappop(self._waiters)
80
152
  if len(manager) == 0:
81
153
  # There are no more waiters, get rid of the empty manager
82
- logger.debug("manager %s has no more waiters, popping from %s", manager._repr_no_parent_(), self)
154
+ logger.debug(
155
+ "manager %s has no more waiters, popping from %s",
156
+ manager._repr_no_parent_(),
157
+ self,
158
+ )
83
159
  self._context_managers.pop(manager._priority)
84
160
  continue
85
161
  logger.debug("waking up next for %s", manager._repr_no_parent_())
86
-
162
+
87
163
  woke_up = False
88
164
  start_len = len(manager)
89
-
165
+
90
166
  if not manager._waiters:
91
- logger.debug('not manager._waiters')
92
-
167
+ logger.debug("not manager._waiters")
168
+
93
169
  while manager._waiters:
94
170
  waiter = manager._waiters.popleft()
95
171
  self._potential_lost_waiters.remove(waiter)
@@ -98,15 +174,15 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
98
174
  logger.debug("woke up %s", waiter)
99
175
  woke_up = True
100
176
  break
101
-
177
+
102
178
  if not woke_up:
103
179
  self._context_managers.pop(manager._priority)
104
180
  continue
105
-
181
+
106
182
  end_len = len(manager)
107
-
183
+
108
184
  assert start_len > end_len, f"start {start_len} end {end_len}"
109
-
185
+
110
186
  if end_len:
111
187
  # There are still waiters, put the manager back
112
188
  heapq.heappush(self._waiters, manager) # type: ignore [misc]
@@ -114,57 +190,94 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
114
190
  # There are no more waiters, get rid of the empty manager
115
191
  self._context_managers.pop(manager._priority)
116
192
  return
117
-
118
- # emergency procedure (hopefully temporary):
193
+
194
+ # emergency procedure (hopefully temporary):
119
195
  while self._potential_lost_waiters:
120
196
  waiter = self._potential_lost_waiters.pop(0)
121
- logger.debug('we found a lost waiter %s', waiter)
197
+ logger.debug("we found a lost waiter %s", waiter)
122
198
  if not waiter.done():
123
199
  waiter.set_result(None)
124
200
  logger.debug("woke up lost waiter %s", waiter)
125
201
  return
126
202
  logger.debug("%s has no waiters to wake", self)
127
203
 
204
+
128
205
  class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
206
+ """
207
+ A context manager for priority semaphore waiters.
208
+
209
+ This context manager is associated with a specific priority and handles
210
+ the acquisition and release of the semaphore for waiters with that priority.
211
+ """
212
+
129
213
  _loop: asyncio.AbstractEventLoop
130
214
  _waiters: Deque[asyncio.Future] # type: ignore [assignment]
131
215
  __slots__ = "_parent", "_priority"
132
-
216
+
133
217
  @property
134
218
  def _priority_name(self) -> str:
135
219
  raise NotImplementedError
136
-
137
- def __init__(self, parent: _AbstractPrioritySemaphore, priority: PT, name: Optional[str] = None) -> None:
220
+
221
+ def __init__(
222
+ self,
223
+ parent: _AbstractPrioritySemaphore,
224
+ priority: PT,
225
+ name: Optional[str] = None,
226
+ ) -> None:
227
+ """Initializes the context manager for a specific priority.
228
+
229
+ Args:
230
+ parent: The parent semaphore.
231
+ priority: The priority associated with this context manager.
232
+ name: An optional name for the context manager, used for debugging.
233
+ """
234
+
138
235
  self._parent = parent
236
+ """The parent semaphore."""
237
+
139
238
  self._priority = priority
239
+ """The priority associated with this context manager."""
240
+
140
241
  super().__init__(0, name=name)
141
242
 
142
243
  def __repr__(self) -> str:
244
+ """Returns a string representation of the context manager."""
143
245
  return f"<{self.__class__.__name__} parent={self._parent} {self._priority_name}={self._priority} waiters={len(self)}>"
144
-
246
+
145
247
  def _repr_no_parent_(self) -> str:
248
+ """Returns a string representation of the context manager without the parent."""
146
249
  return f"<{self.__class__.__name__} parent_name={self._parent.name} {self._priority_name}={self._priority} waiters={len(self)}>"
147
-
250
+
148
251
  def __lt__(self, other) -> bool:
252
+ """Compares this context manager with another based on priority.
253
+
254
+ Args:
255
+ other: The other context manager to compare with.
256
+
257
+ Returns:
258
+ True if this context manager has a lower priority than the other, False otherwise.
259
+ """
149
260
  if type(other) is not type(self):
150
261
  raise TypeError(f"{other} is not type {self.__class__.__name__}")
151
262
  return self._priority < other._priority
152
-
263
+
153
264
  @cached_property
154
265
  def loop(self) -> asyncio.AbstractEventLoop:
266
+ """Gets the event loop associated with this context manager."""
155
267
  return self._loop or asyncio.get_event_loop()
156
-
268
+
157
269
  @property
158
- def waiters (self) -> Deque[asyncio.Future]:
270
+ def waiters(self) -> Deque[asyncio.Future]:
271
+ """Gets the deque of waiters for this context manager."""
159
272
  if self._waiters is None:
160
273
  self._waiters = deque()
161
274
  return self._waiters
162
-
275
+
163
276
  async def acquire(self) -> Literal[True]:
164
- """Acquire a semaphore.
277
+ """Acquires the semaphore for this context manager.
165
278
 
166
279
  If the internal counter is larger than zero on entry,
167
- decrement it by one and return True immediately. If it is
280
+ decrement it by one and return True immediately. If it is
168
281
  zero on entry, block, waiting until some other coroutine has
169
282
  called release() to make it larger than 0, and then return
170
283
  True.
@@ -185,31 +298,38 @@ class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
185
298
  raise
186
299
  self._parent._value -= 1
187
300
  return True
301
+
188
302
  def release(self) -> None:
303
+ """Releases the semaphore for this context manager."""
189
304
  self._parent.release()
190
-
191
- class _PrioritySemaphoreContextManager(_AbstractPrioritySemaphoreContextManager[Numeric]):
305
+
306
+
307
+ class _PrioritySemaphoreContextManager(
308
+ _AbstractPrioritySemaphoreContextManager[Numeric]
309
+ ):
310
+ """Context manager for numeric priority semaphores."""
311
+
192
312
  _priority_name = "priority"
193
313
 
314
+
194
315
  class PrioritySemaphore(_AbstractPrioritySemaphore[Numeric, _PrioritySemaphoreContextManager]): # type: ignore [type-var]
316
+ """Semaphore that uses numeric priorities for waiters.
317
+
318
+ It's similar to a regular Semaphore but requires each waiter to have a priority:
319
+
320
+ Examples:
321
+ The primary way to use this semaphore is by specifying a priority.
322
+
323
+ >>> priority_semaphore = PrioritySemaphore(10)
324
+ >>> async with priority_semaphore[priority]:
325
+ ... await do_stuff()
326
+
327
+ You can also enter and exit this semaphore without specifying a priority, and it will use the top priority by default:
328
+
329
+ >>> priority_semaphore = PrioritySemaphore(10)
330
+ >>> async with priority_semaphore:
331
+ ... await do_stuff()
332
+ """
333
+
195
334
  _context_manager_class = _PrioritySemaphoreContextManager
196
335
  _top_priority = -1
197
- """
198
- It's kinda like a regular Semaphore but you must give each waiter a priority:
199
-
200
- ```
201
- priority_semaphore = PrioritySemaphore(10)
202
-
203
- async with priority_semaphore[priority]:
204
- await do_stuff()
205
- ```
206
-
207
- You can aenter and aexit this semaphore without a priority and it will process those first. Like so:
208
-
209
- ```
210
- priority_semaphore = PrioritySemaphore(10)
211
-
212
- async with priority_semaphore:
213
- await do_stuff()
214
- ```
215
- """