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
@@ -1,3 +1,8 @@
1
+ """
2
+ This module provides various semaphore implementations, including a debug-enabled semaphore,
3
+ a dummy semaphore that does nothing, and a threadsafe semaphore for use in multi-threaded applications.
4
+ """
5
+
1
6
  import asyncio
2
7
  import functools
3
8
  import logging
@@ -10,17 +15,19 @@ from a_sync.primitives._debug import _DebugDaemonMixin
10
15
 
11
16
  logger = logging.getLogger(__name__)
12
17
 
18
+
13
19
  class Semaphore(asyncio.Semaphore, _DebugDaemonMixin):
14
20
  """
15
21
  A semaphore with additional debugging capabilities.
16
-
17
- This semaphore includes debug logging.
18
-
19
- Also, it can be used to decorate coroutine functions so you can rewrite this pattern:
22
+
23
+ This semaphore includes debug logging and can be used to decorate coroutine functions.
24
+ It allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
25
+
26
+ So you can write this pattern:
20
27
 
21
28
  ```
22
29
  semaphore = Semaphore(5)
23
-
30
+
24
31
  async def limited():
25
32
  async with semaphore:
26
33
  return 1
@@ -37,91 +44,67 @@ class Semaphore(asyncio.Semaphore, _DebugDaemonMixin):
37
44
  return 1
38
45
  ```
39
46
  """
47
+
40
48
  if sys.version_info >= (3, 10):
41
49
  __slots__ = "name", "_value", "_waiters", "_decorated"
42
50
  else:
43
51
  __slots__ = "name", "_value", "_waiters", "_loop", "_decorated"
44
-
52
+
45
53
  def __init__(self, value: int, name=None, **kwargs) -> None:
46
54
  """
47
55
  Initialize the semaphore with a given value and optional name for debugging.
48
-
56
+
49
57
  Args:
50
58
  value: The initial value for the semaphore.
51
59
  name (optional): An optional name used only to provide useful context in debug logs.
52
60
  """
53
61
  super().__init__(value, **kwargs)
54
- self.name = name or self.__origin__ if hasattr(self, '__origin__') else None
62
+ self.name = name or self.__origin__ if hasattr(self, "__origin__") else None
55
63
  self._decorated: Set[str] = set()
56
-
57
- # Dank new functionality
58
64
 
59
65
  def __call__(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
60
66
  """
61
- Convenient decorator method to wrap coroutine functions with the semaphore so you can rewrite this pattern:
67
+ Decorator method to wrap coroutine functions with the semaphore.
62
68
 
63
- ```
64
- semaphore = Semaphore(5)
65
-
66
- async def limited():
67
- async with semaphore:
68
- return 1
69
-
70
- ```
69
+ This allows rewriting the pattern of acquiring a semaphore within a coroutine using a decorator.
71
70
 
72
- like this:
73
-
74
- ```
75
- semaphore = Semaphore(5)
71
+ Example:
72
+ semaphore = Semaphore(5)
76
73
 
77
- @semaphore
78
- async def limited():
79
- return 1
80
- ```
74
+ @semaphore
75
+ async def limited():
76
+ return 1
81
77
  """
82
78
  return self.decorate(fn) # type: ignore [arg-type, return-value]
83
-
79
+
84
80
  def __repr__(self) -> str:
85
81
  representation = f"<{self.__class__.__name__} name={self.name} value={self._value} waiters={len(self)}>"
86
82
  if self._decorated:
87
83
  representation = f"{representation[:-1]} decorates={self._decorated}"
88
84
  return representation
89
-
85
+
90
86
  def __len__(self) -> int:
91
87
  return len(self._waiters) if self._waiters else 0
92
-
88
+
93
89
  def decorate(self, fn: CoroFn[P, T]) -> CoroFn[P, T]:
94
90
  """
95
91
  Wrap a coroutine function to ensure it runs with the semaphore.
96
-
97
- Example:
98
- Now you can rewrite this pattern:
99
-
100
- ```
101
- semaphore = Semaphore(5)
102
-
103
- async def limited():
104
- async with semaphore:
105
- return 1
106
92
 
107
- ```
108
-
109
- like this:
110
-
111
- ```
93
+ Example:
112
94
  semaphore = Semaphore(5)
113
95
 
114
96
  @semaphore
115
97
  async def limited():
116
98
  return 1
117
- ```
118
99
  """
119
100
  if not asyncio.iscoroutinefunction(fn):
120
101
  raise TypeError(f"{fn} must be a coroutine function")
102
+
121
103
  @functools.wraps(fn)
122
104
  async def semaphore_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
123
105
  async with self:
124
106
  return await fn(*args, **kwargs)
107
+
125
108
  self._decorated.add(f"{fn.__module__}.{fn.__name__}")
126
109
  return semaphore_wrapper
127
110
 
@@ -129,79 +112,97 @@ class Semaphore(asyncio.Semaphore, _DebugDaemonMixin):
129
112
  if self._value <= 0:
130
113
  self._ensure_debug_daemon()
131
114
  return await super().acquire()
132
-
133
- # Everything below just adds some debug logs
115
+
134
116
  async def _debug_daemon(self) -> None:
135
117
  """
136
118
  Daemon coroutine (runs in a background task) which will emit a debug log every minute while the semaphore has waiters.
137
119
  """
138
120
  while self._waiters:
139
121
  await asyncio.sleep(60)
140
- self.logger.debug(f"{self} has {len(self)} waiters for any of: {self._decorated}")
141
-
142
-
122
+ self.logger.debug(
123
+ f"{self} has {len(self)} waiters for any of: {self._decorated}"
124
+ )
125
+
126
+
143
127
  class DummySemaphore(asyncio.Semaphore):
144
128
  """
145
129
  A dummy semaphore that implements the standard :class:`asyncio.Semaphore` API but does nothing.
130
+
131
+ This class is useful for scenarios where a semaphore interface is required but no actual synchronization is needed.
146
132
  """
147
133
 
148
134
  __slots__ = "name", "_value"
149
-
135
+
150
136
  def __init__(self, name: Optional[str] = None):
137
+ """
138
+ Initialize the dummy semaphore with an optional name.
139
+
140
+ Args:
141
+ name (optional): An optional name for the dummy semaphore.
142
+ """
151
143
  self.name = name
152
144
  self._value = 0
153
-
145
+
154
146
  def __repr__(self) -> str:
155
147
  return f"<{self.__class__.__name__} name={self.name}>"
156
-
148
+
157
149
  async def acquire(self) -> Literal[True]:
158
150
  return True
159
-
160
- def release(self) -> None:
161
- ...
162
-
163
- async def __aenter__(self):
164
- ...
165
-
166
- async def __aexit__(self, *args):
167
- ...
168
-
151
+
152
+ def release(self) -> None: ...
153
+
154
+ async def __aenter__(self): ...
155
+
156
+ async def __aexit__(self, *args): ...
157
+
169
158
 
170
159
  class ThreadsafeSemaphore(Semaphore):
171
160
  """
172
- While its a bit weird to run multiple event loops, sometimes either you or a lib you're using must do so.
173
- When in use in threaded applications, this semaphore will not work as intended but at least your program will function.
174
- You may need to reduce the semaphore value for multi-threaded applications.
175
-
176
- # TL;DR it's a janky fix for an edge case problem and will otherwise function as a normal a_sync.Semaphore (which is just an asyncio.Semaphore with extra bells and whistles).
161
+ A semaphore that works in a multi-threaded environment.
162
+
163
+ This semaphore ensures that the program functions correctly even when used with multiple event loops.
164
+ It provides a workaround for edge cases involving multiple threads and event loops.
177
165
  """
166
+
178
167
  __slots__ = "semaphores", "dummy"
179
-
168
+
180
169
  def __init__(self, value: Optional[int], name: Optional[str] = None) -> None:
170
+ """
171
+ Initialize the threadsafe semaphore with a given value and optional name.
172
+
173
+ Args:
174
+ value: The initial value for the semaphore, should be an integer.
175
+ name (optional): An optional name for the semaphore.
176
+ """
181
177
  assert isinstance(value, int), f"{value} should be an integer."
182
178
  super().__init__(value, name=name)
183
179
  self.semaphores: DefaultDict[Thread, Semaphore] = defaultdict(lambda: Semaphore(value, name=self.name)) # type: ignore [arg-type]
184
180
  self.dummy = DummySemaphore(name=name)
185
-
181
+
186
182
  def __len__(self) -> int:
187
183
  return sum(len(sem._waiters) for sem in self.semaphores.values())
188
-
184
+
189
185
  @functools.cached_property
190
186
  def use_dummy(self) -> bool:
187
+ """
188
+ Determine whether to use a dummy semaphore.
189
+
190
+ Returns:
191
+ True if the semaphore value is None, indicating the use of a dummy semaphore.
192
+ """
191
193
  return self._value is None
192
-
194
+
193
195
  @property
194
196
  def semaphore(self) -> Semaphore:
195
197
  """
196
198
  Returns the appropriate semaphore for the current thread.
197
-
199
+
198
200
  NOTE: We can't cache this property because we need to check the current thread every time we access it.
199
201
  """
200
202
  return self.dummy if self.use_dummy else self.semaphores[current_thread()]
201
-
203
+
202
204
  async def __aenter__(self):
203
205
  await self.semaphore.acquire()
204
-
206
+
205
207
  async def __aexit__(self, *args):
206
208
  self.semaphore.release()
207
-