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
@@ -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,208 @@ 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. 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.
40
+ """
41
+
23
42
  name: Optional[str]
24
43
  _value: int
25
44
  _waiters: List["_AbstractPrioritySemaphoreContextManager[PT]"] # type: ignore [assignment]
26
- __slots__ = "name", "_value", "_waiters", "_context_managers", "_capacity", "_potential_lost_waiters"
45
+ _context_managers: Dict[PT, "_AbstractPrioritySemaphoreContextManager[PT]"]
46
+ __slots__ = (
47
+ "name",
48
+ "_value",
49
+ "_waiters",
50
+ "_context_managers",
51
+ "_capacity",
52
+ "_potential_lost_waiters",
53
+ )
27
54
 
28
55
  @property
29
- def _context_manager_class(self) -> Type["_AbstractPrioritySemaphoreContextManager[PT]"]:
56
+ def _context_manager_class(
57
+ self,
58
+ ) -> Type["_AbstractPrioritySemaphoreContextManager[PT]"]:
30
59
  raise NotImplementedError
31
-
60
+
32
61
  @property
33
62
  def _top_priority(self) -> PT:
34
- # You can use this so you can set priorities with non numeric comparable values
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
+ """
35
70
  raise NotImplementedError
36
71
 
37
72
  def __init__(self, value: int = 1, *, name: Optional[str] = None) -> None:
38
- self._context_managers: Dict[PT, _AbstractPrioritySemaphoreContextManager[PT]] = {}
73
+ """Initializes the priority semaphore.
74
+
75
+ Args:
76
+ value: The initial capacity of the semaphore.
77
+ name: An optional name for the semaphore, used for debugging.
78
+
79
+ Examples:
80
+ >>> semaphore = _AbstractPrioritySemaphore(5, name="test_semaphore")
81
+ """
82
+
83
+ self._context_managers = {}
84
+ """A dictionary mapping priorities to their context managers."""
85
+
39
86
  self._capacity = value
87
+ """The initial capacity of the semaphore."""
88
+
40
89
  super().__init__(value, name=name)
41
90
  self._waiters = []
91
+ """A heap queue of context managers, sorted by priority."""
92
+
42
93
  # NOTE: This should (hopefully) be temporary
43
94
  self._potential_lost_waiters: List["asyncio.Future[None]"] = []
95
+ """A list of futures representing waiters that might have been lost."""
44
96
 
45
97
  def __repr__(self) -> str:
98
+ """Returns a string representation of the semaphore."""
46
99
  return f"<{self.__class__.__name__} name={self.name} capacity={self._capacity} value={self._value} waiters={self._count_waiters()}>"
47
100
 
48
101
  async def __aenter__(self) -> None:
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
+ """
49
111
  await self[self._top_priority].acquire()
50
112
 
51
113
  async def __aexit__(self, *_) -> None:
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
+ """
52
123
  self[self._top_priority].release()
53
-
124
+
54
125
  async def acquire(self) -> Literal[True]:
126
+ """Acquires the semaphore with the top priority.
127
+
128
+ Examples:
129
+ >>> semaphore = _AbstractPrioritySemaphore(5)
130
+ >>> await semaphore.acquire()
131
+ """
55
132
  return await self[self._top_priority].acquire()
56
-
57
- def __getitem__(self, priority: Optional[PT]) -> "_AbstractPrioritySemaphoreContextManager[PT]":
133
+
134
+ def __getitem__(
135
+ self, priority: Optional[PT]
136
+ ) -> "_AbstractPrioritySemaphoreContextManager[PT]":
137
+ """Gets the context manager for a given priority.
138
+
139
+ Args:
140
+ priority: The priority for which to get the context manager. If None, uses the top priority.
141
+
142
+ Returns:
143
+ The context manager associated with the given priority.
144
+
145
+ Examples:
146
+ >>> semaphore = _AbstractPrioritySemaphore(5)
147
+ >>> context_manager = semaphore[priority]
148
+ """
58
149
  priority = self._top_priority if priority is None else priority
59
150
  if priority not in self._context_managers:
60
- context_manager = self._context_manager_class(self, priority, name=self.name)
151
+ context_manager = self._context_manager_class(
152
+ self, priority, name=self.name
153
+ )
61
154
  heapq.heappush(self._waiters, context_manager) # type: ignore [misc]
62
155
  self._context_managers[priority] = context_manager
63
156
  return self._context_managers[priority]
64
157
 
65
158
  def locked(self) -> bool:
66
- """Returns True if semaphore cannot be acquired immediately."""
159
+ """Checks if the semaphore is locked.
160
+
161
+ Returns:
162
+ True if the semaphore cannot be acquired immediately, False otherwise.
163
+
164
+ Examples:
165
+ >>> semaphore = _AbstractPrioritySemaphore(5)
166
+ >>> semaphore.locked()
167
+ """
67
168
  return self._value == 0 or (
68
169
  any(
69
- cm._waiters and any(not w.cancelled() for w in cm._waiters)
170
+ cm._waiters and any(not w.cancelled() for w in cm._waiters)
70
171
  for cm in (self._context_managers.values() or ())
71
172
  )
72
173
  )
73
-
174
+
74
175
  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
-
176
+ """Counts the number of waiters for each priority.
177
+
178
+ Returns:
179
+ A dictionary mapping each priority to the number of waiters.
180
+
181
+ Examples:
182
+ >>> semaphore = _AbstractPrioritySemaphore(5)
183
+ >>> semaphore._count_waiters()
184
+ """
185
+ return {
186
+ manager._priority: len(manager.waiters)
187
+ for manager in sorted(self._waiters, key=lambda m: m._priority)
188
+ }
189
+
77
190
  def _wake_up_next(self) -> None:
191
+ """Wakes up the next waiter in line.
192
+
193
+ This method handles the waking of waiters based on priority. It includes an emergency
194
+ procedure to handle potential lost waiters, ensuring that no waiter is left indefinitely
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()
202
+ """
78
203
  while self._waiters:
79
204
  manager = heapq.heappop(self._waiters)
80
205
  if len(manager) == 0:
81
206
  # 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)
207
+ logger.debug(
208
+ "manager %s has no more waiters, popping from %s",
209
+ manager._repr_no_parent_(),
210
+ self,
211
+ )
83
212
  self._context_managers.pop(manager._priority)
84
213
  continue
85
214
  logger.debug("waking up next for %s", manager._repr_no_parent_())
86
-
215
+
87
216
  woke_up = False
88
217
  start_len = len(manager)
89
-
218
+
90
219
  if not manager._waiters:
91
- logger.debug('not manager._waiters')
92
-
220
+ logger.debug("not manager._waiters")
221
+
93
222
  while manager._waiters:
94
223
  waiter = manager._waiters.popleft()
95
224
  self._potential_lost_waiters.remove(waiter)
@@ -98,15 +227,15 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
98
227
  logger.debug("woke up %s", waiter)
99
228
  woke_up = True
100
229
  break
101
-
230
+
102
231
  if not woke_up:
103
232
  self._context_managers.pop(manager._priority)
104
233
  continue
105
-
234
+
106
235
  end_len = len(manager)
107
-
236
+
108
237
  assert start_len > end_len, f"start {start_len} end {end_len}"
109
-
238
+
110
239
  if end_len:
111
240
  # There are still waiters, put the manager back
112
241
  heapq.heappush(self._waiters, manager) # type: ignore [misc]
@@ -114,60 +243,113 @@ class _AbstractPrioritySemaphore(Semaphore, Generic[PT, CM]):
114
243
  # There are no more waiters, get rid of the empty manager
115
244
  self._context_managers.pop(manager._priority)
116
245
  return
117
-
118
- # emergency procedure (hopefully temporary):
246
+
247
+ # emergency procedure (hopefully temporary):
119
248
  while self._potential_lost_waiters:
120
249
  waiter = self._potential_lost_waiters.pop(0)
121
- logger.debug('we found a lost waiter %s', waiter)
250
+ logger.debug("we found a lost waiter %s", waiter)
122
251
  if not waiter.done():
123
252
  waiter.set_result(None)
124
253
  logger.debug("woke up lost waiter %s", waiter)
125
254
  return
126
255
  logger.debug("%s has no waiters to wake", self)
127
256
 
257
+
128
258
  class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
259
+ """
260
+ A context manager for priority semaphore waiters.
261
+
262
+ This context manager is associated with a specific priority and handles
263
+ the acquisition and release of the semaphore for waiters with that priority.
264
+ """
265
+
129
266
  _loop: asyncio.AbstractEventLoop
130
267
  _waiters: Deque[asyncio.Future] # type: ignore [assignment]
131
268
  __slots__ = "_parent", "_priority"
132
-
269
+
133
270
  @property
134
271
  def _priority_name(self) -> str:
135
272
  raise NotImplementedError
136
-
137
- def __init__(self, parent: _AbstractPrioritySemaphore, priority: PT, name: Optional[str] = None) -> None:
273
+
274
+ def __init__(
275
+ self,
276
+ parent: _AbstractPrioritySemaphore,
277
+ priority: PT,
278
+ name: Optional[str] = None,
279
+ ) -> None:
280
+ """Initializes the context manager for a specific priority.
281
+
282
+ Args:
283
+ parent: The parent semaphore.
284
+ priority: The priority associated with this context manager.
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)
290
+ """
291
+
138
292
  self._parent = parent
293
+ """The parent semaphore."""
294
+
139
295
  self._priority = priority
296
+ """The priority associated with this context manager."""
297
+
140
298
  super().__init__(0, name=name)
141
299
 
142
300
  def __repr__(self) -> str:
301
+ """Returns a string representation of the context manager."""
143
302
  return f"<{self.__class__.__name__} parent={self._parent} {self._priority_name}={self._priority} waiters={len(self)}>"
144
-
303
+
145
304
  def _repr_no_parent_(self) -> str:
305
+ """Returns a string representation of the context manager without the parent."""
146
306
  return f"<{self.__class__.__name__} parent_name={self._parent.name} {self._priority_name}={self._priority} waiters={len(self)}>"
147
-
307
+
148
308
  def __lt__(self, other) -> bool:
309
+ """Compares this context manager with another based on priority.
310
+
311
+ Args:
312
+ other: The other context manager to compare with.
313
+
314
+ Returns:
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
324
+ """
149
325
  if type(other) is not type(self):
150
326
  raise TypeError(f"{other} is not type {self.__class__.__name__}")
151
327
  return self._priority < other._priority
152
-
328
+
153
329
  @cached_property
154
330
  def loop(self) -> asyncio.AbstractEventLoop:
331
+ """Gets the event loop associated with this context manager."""
155
332
  return self._loop or asyncio.get_event_loop()
156
-
333
+
157
334
  @property
158
- def waiters (self) -> Deque[asyncio.Future]:
335
+ def waiters(self) -> Deque[asyncio.Future]:
336
+ """Gets the deque of waiters for this context manager."""
159
337
  if self._waiters is None:
160
338
  self._waiters = deque()
161
339
  return self._waiters
162
-
340
+
163
341
  async def acquire(self) -> Literal[True]:
164
- """Acquire a semaphore.
342
+ """Acquires the semaphore for this context manager.
165
343
 
166
344
  If the internal counter is larger than zero on entry,
167
- decrement it by one and return True immediately. If it is
345
+ decrement it by one and return True immediately. If it is
168
346
  zero on entry, block, waiting until some other coroutine has
169
347
  called release() to make it larger than 0, and then return
170
348
  True.
349
+
350
+ Examples:
351
+ >>> context_manager = _AbstractPrioritySemaphoreContextManager(parent, priority=1)
352
+ >>> await context_manager.acquire()
171
353
  """
172
354
  if self._parent._value <= 0:
173
355
  self._ensure_debug_daemon()
@@ -185,31 +367,48 @@ class _AbstractPrioritySemaphoreContextManager(Semaphore, Generic[PT]):
185
367
  raise
186
368
  self._parent._value -= 1
187
369
  return True
370
+
188
371
  def release(self) -> None:
372
+ """Releases the semaphore for this context manager.
373
+
374
+ Examples:
375
+ >>> context_manager = _AbstractPrioritySemaphoreContextManager(parent, priority=1)
376
+ >>> context_manager.release()
377
+ """
189
378
  self._parent.release()
190
-
191
- class _PrioritySemaphoreContextManager(_AbstractPrioritySemaphoreContextManager[Numeric]):
379
+
380
+
381
+ class _PrioritySemaphoreContextManager(
382
+ _AbstractPrioritySemaphoreContextManager[Numeric]
383
+ ):
384
+ """Context manager for numeric priority semaphores."""
385
+
192
386
  _priority_name = "priority"
193
387
 
388
+
194
389
  class PrioritySemaphore(_AbstractPrioritySemaphore[Numeric, _PrioritySemaphoreContextManager]): # type: ignore [type-var]
390
+ """Semaphore that uses numeric priorities for waiters.
391
+
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.
395
+
396
+ Examples:
397
+ The primary way to use this semaphore is by specifying a priority.
398
+
399
+ >>> priority_semaphore = PrioritySemaphore(10)
400
+ >>> async with priority_semaphore[priority]:
401
+ ... await do_stuff()
402
+
403
+ You can also enter and exit this semaphore without specifying a priority, and it will use the top priority by default:
404
+
405
+ >>> priority_semaphore = PrioritySemaphore(10)
406
+ >>> async with priority_semaphore:
407
+ ... await do_stuff()
408
+
409
+ See Also:
410
+ :class:`_AbstractPrioritySemaphore` for the base class implementation.
411
+ """
412
+
195
413
  _context_manager_class = _PrioritySemaphoreContextManager
196
414
  _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
- """