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
@@ -5,9 +5,47 @@ from a_sync._typing import *
5
5
  from a_sync.a_sync.config import user_set_default_modifiers, null_modifiers
6
6
  from a_sync.a_sync.modifiers import cache, limiter, semaphores
7
7
 
8
- valid_modifiers = [key for key in ModifierKwargs.__annotations__ if not key.startswith('_') and not key.endswith('_')]
8
+ # TODO give me a docstring
9
+ valid_modifiers = [
10
+ key
11
+ for key in ModifierKwargs.__annotations__
12
+ if not key.startswith("_") and not key.endswith("_")
13
+ ]
14
+
9
15
 
10
16
  class ModifierManager(Dict[str, Any]):
17
+ """Manages modifiers for asynchronous and synchronous functions.
18
+
19
+ This class is responsible for applying modifiers to functions, such as
20
+ caching, rate limiting, and semaphores for asynchronous functions. It also
21
+ handles synchronous functions, although no sync modifiers are currently
22
+ implemented.
23
+
24
+ Examples:
25
+ Creating a ModifierManager with specific modifiers:
26
+
27
+ >>> modifiers = ModifierKwargs(cache_type='memory', runs_per_minute=60)
28
+ >>> manager = ModifierManager(modifiers)
29
+
30
+ Applying modifiers to an asynchronous function:
31
+
32
+ >>> async def my_coro():
33
+ ... pass
34
+ >>> modified_coro = manager.apply_async_modifiers(my_coro)
35
+
36
+ Applying modifiers to a synchronous function (no sync modifiers applied):
37
+
38
+ >>> def my_function():
39
+ ... pass
40
+ >>> modified_function = manager.apply_sync_modifiers(my_function)
41
+
42
+ See Also:
43
+ - :class:`a_sync.a_sync.modifiers.cache`
44
+ - :class:`a_sync.a_sync.modifiers.limiter`
45
+ - :class:`a_sync.a_sync.modifiers.semaphores`
46
+ """
47
+
48
+ # TODO give us docstrings
11
49
  default: DefaultMode
12
50
  cache_type: CacheType
13
51
  cache_typed: bool
@@ -15,38 +53,86 @@ class ModifierManager(Dict[str, Any]):
15
53
  ram_cache_ttl: Optional[int]
16
54
  runs_per_minute: Optional[int]
17
55
  semaphore: SemaphoreSpec
56
+
18
57
  # sync modifiers
19
58
  executor: Executor
20
- __slots__ = "_modifiers",
59
+ """
60
+ This is not applied like a typical modifier but is still passed through the library with them for convenience.
61
+ The executor is used to run the sync function in an asynchronous context.
62
+ """
63
+
64
+ __slots__ = ("_modifiers",)
65
+
21
66
  def __init__(self, modifiers: ModifierKwargs) -> None:
67
+ """Initializes the ModifierManager with the given modifiers.
68
+
69
+ Args:
70
+ modifiers: A dictionary of modifiers to be applied.
71
+
72
+ Raises:
73
+ ValueError: If an unsupported modifier is provided.
74
+ """
22
75
  for key in modifiers.keys():
23
76
  if key not in valid_modifiers:
24
77
  raise ValueError(f"'{key}' is not a supported modifier.")
25
78
  self._modifiers = modifiers
79
+
26
80
  def __repr__(self) -> str:
81
+ """Returns a string representation of the modifiers."""
27
82
  return str(self._modifiers)
83
+
28
84
  def __getattribute__(self, modifier_key: str) -> Any:
85
+ """Gets the value of a modifier.
86
+
87
+ Args:
88
+ modifier_key: The key of the modifier to retrieve.
89
+
90
+ Returns:
91
+ The value of the modifier, or the default value if not set.
92
+ """
29
93
  if modifier_key not in valid_modifiers:
30
94
  return super().__getattribute__(modifier_key)
31
- return self[modifier_key] if modifier_key in self else user_defaults[modifier_key]
95
+ return (
96
+ self[modifier_key] if modifier_key in self else user_defaults[modifier_key]
97
+ )
32
98
 
33
-
34
99
  @property
35
100
  def use_limiter(self) -> bool:
101
+ """Determines if a rate limiter should be used."""
36
102
  return self.runs_per_minute != nulls.runs_per_minute
103
+
37
104
  @property
38
105
  def use_semaphore(self) -> bool:
106
+ """Determines if a semaphore should be used."""
39
107
  return self.semaphore != nulls.semaphore
108
+
40
109
  @property
41
110
  def use_cache(self) -> bool:
42
- return any([
43
- self.cache_type != nulls.cache_type,
44
- self.ram_cache_maxsize != nulls.ram_cache_maxsize,
45
- self.ram_cache_ttl != nulls.ram_cache_ttl,
46
- self.cache_typed != nulls.cache_typed,
47
- ])
48
-
111
+ """Determines if caching should be used."""
112
+ return any(
113
+ [
114
+ self.cache_type != nulls.cache_type,
115
+ self.ram_cache_maxsize != nulls.ram_cache_maxsize,
116
+ self.ram_cache_ttl != nulls.ram_cache_ttl,
117
+ self.cache_typed != nulls.cache_typed,
118
+ ]
119
+ )
120
+
49
121
  def apply_async_modifiers(self, coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
122
+ """Applies asynchronous modifiers to a coroutine function.
123
+
124
+ Args:
125
+ coro_fn: The coroutine function to modify.
126
+
127
+ Returns:
128
+ The modified coroutine function.
129
+
130
+ Examples:
131
+ >>> async def my_coro():
132
+ ... pass
133
+ >>> manager = ModifierManager(ModifierKwargs(runs_per_minute=60))
134
+ >>> modified_coro = manager.apply_async_modifiers(my_coro)
135
+ """
50
136
  # NOTE: THESE STACK IN REVERSE ORDER
51
137
  if self.use_limiter:
52
138
  coro_fn = limiter.apply_rate_limit(coro_fn, self.runs_per_minute)
@@ -55,35 +141,75 @@ class ModifierManager(Dict[str, Any]):
55
141
  if self.use_cache:
56
142
  coro_fn = cache.apply_async_cache(
57
143
  coro_fn,
58
- cache_type=self.cache_type or 'memory',
144
+ cache_type=self.cache_type or "memory",
59
145
  cache_typed=self.cache_typed,
60
146
  ram_cache_maxsize=self.ram_cache_maxsize,
61
- ram_cache_ttl=self.ram_cache_ttl
147
+ ram_cache_ttl=self.ram_cache_ttl,
62
148
  )
63
149
  return coro_fn
64
-
150
+
65
151
  def apply_sync_modifiers(self, function: SyncFn[P, T]) -> SyncFn[P, T]:
152
+ """Wraps a synchronous function.
153
+
154
+ Note:
155
+ There are no sync modifiers at this time, but they will be added here for convenience.
156
+
157
+ Args:
158
+ function: The synchronous function to wrap.
159
+
160
+ Returns:
161
+ The wrapped synchronous function.
162
+
163
+ Examples:
164
+ >>> def my_function():
165
+ ... pass
166
+ >>> manager = ModifierManager(ModifierKwargs())
167
+ >>> modified_function = manager.apply_sync_modifiers(my_function)
168
+ """
169
+
66
170
  @functools.wraps(function)
67
171
  def sync_modifier_wrap(*args: P.args, **kwargs: P.kwargs) -> T:
68
172
  return function(*args, **kwargs)
69
- # NOTE There are no sync modifiers at this time but they will be added here for my convenience.
173
+
70
174
  return sync_modifier_wrap
71
-
175
+
72
176
  # Dictionary api
73
177
  def keys(self) -> KeysView[str]: # type: ignore [override]
178
+ """Returns the keys of the modifiers."""
74
179
  return self._modifiers.keys()
180
+
75
181
  def values(self) -> ValuesView[Any]: # type: ignore [override]
182
+ """Returns the values of the modifiers."""
76
183
  return self._modifiers.values()
184
+
77
185
  def items(self) -> ItemsView[str, Any]: # type: ignore [override]
186
+ """Returns the items of the modifiers."""
78
187
  return self._modifiers.items()
188
+
79
189
  def __contains__(self, key: str) -> bool: # type: ignore [override]
190
+ """Checks if a key is in the modifiers."""
80
191
  return key in self._modifiers
192
+
81
193
  def __iter__(self) -> Iterator[str]:
194
+ """Returns an iterator over the modifier keys."""
82
195
  return self._modifiers.__iter__()
196
+
83
197
  def __len__(self) -> int:
198
+ """Returns the number of modifiers."""
84
199
  return len(self._modifiers)
200
+
85
201
  def __getitem__(self, modifier_key: str):
202
+ """Gets the value of a modifier by key.
203
+
204
+ Args:
205
+ modifier_key: The key of the modifier to retrieve.
206
+
207
+ Returns:
208
+ The value of the modifier.
209
+ """
86
210
  return self._modifiers[modifier_key] # type: ignore [literal-required]
87
211
 
212
+
213
+ # TODO give us docstrings
88
214
  nulls = ModifierManager(null_modifiers)
89
215
  user_defaults = ModifierManager(user_set_default_modifiers)
@@ -12,58 +12,164 @@ from a_sync.primitives import ThreadsafeSemaphore, DummySemaphore
12
12
 
13
13
  @overload
14
14
  def apply_semaphore( # type: ignore [misc]
15
- coro_fn: Literal[None],
16
15
  semaphore: SemaphoreSpec,
17
- ) -> AsyncDecorator[P, T]:...
16
+ ) -> AsyncDecorator[P, T]:
17
+ """Create a decorator to apply a semaphore to a coroutine function.
18
+
19
+ This overload is used when the semaphore is provided as a single argument,
20
+ returning a decorator that can be applied to a coroutine function.
21
+
22
+ Args:
23
+ semaphore (Union[int, asyncio.Semaphore, primitives.Semaphore]):
24
+ The semaphore to apply, which can be an integer, an `asyncio.Semaphore`, or a `primitives.Semaphore`.
25
+
26
+ Examples:
27
+ Using as a decorator with an integer semaphore:
28
+ >>> @apply_semaphore(2)
29
+ ... async def limited_concurrent_function():
30
+ ... pass
31
+
32
+ Using as a decorator with an `asyncio.Semaphore`:
33
+ >>> sem = asyncio.Semaphore(2)
34
+ >>> @apply_semaphore(sem)
35
+ ... async def another_function():
36
+ ... pass
37
+
38
+ Using as a decorator with a `primitives.Semaphore`:
39
+ >>> sem = primitives.ThreadsafeSemaphore(2)
40
+ >>> @apply_semaphore(sem)
41
+ ... async def yet_another_function():
42
+ ... pass
43
+
44
+ See Also:
45
+ - :class:`asyncio.Semaphore`
46
+ - :class:`primitives.Semaphore`
47
+
48
+ Note:
49
+ `primitives.Semaphore` is a subclass of `asyncio.Semaphore`. Therefore, when the documentation refers to `asyncio.Semaphore`, it also includes `primitives.Semaphore` and any other subclasses.
50
+ """
18
51
 
19
- @overload
20
- def apply_semaphore(
21
- coro_fn: SemaphoreSpec,
22
- semaphore: Literal[None],
23
- ) -> AsyncDecorator[P, T]:...
24
52
 
25
53
  @overload
26
54
  def apply_semaphore(
27
55
  coro_fn: CoroFn[P, T],
28
56
  semaphore: SemaphoreSpec,
29
- ) -> CoroFn[P, T]:...
30
-
57
+ ) -> CoroFn[P, T]:
58
+ """Apply a semaphore directly to a coroutine function.
59
+
60
+ This overload is used when both the coroutine function and semaphore are provided,
61
+ directly applying the semaphore to the coroutine function.
62
+
63
+ Args:
64
+ coro_fn (Callable): The coroutine function to which the semaphore will be applied.
65
+ semaphore (Union[int, asyncio.Semaphore, primitives.Semaphore]):
66
+ The semaphore to apply, which can be an integer, an `asyncio.Semaphore`, or a `primitives.Semaphore`.
67
+
68
+ Examples:
69
+ Applying directly to a function with an integer semaphore:
70
+ >>> async def my_coroutine():
71
+ ... pass
72
+ >>> my_coroutine = apply_semaphore(my_coroutine, 3)
73
+
74
+ Applying directly with an `asyncio.Semaphore`:
75
+ >>> sem = asyncio.Semaphore(3)
76
+ >>> my_coroutine = apply_semaphore(my_coroutine, sem)
77
+
78
+ Applying directly with a `primitives.Semaphore`:
79
+ >>> sem = primitives.ThreadsafeSemaphore(3)
80
+ >>> my_coroutine = apply_semaphore(my_coroutine, sem)
81
+
82
+ See Also:
83
+ - :class:`asyncio.Semaphore`
84
+ - :class:`primitives.Semaphore`
85
+
86
+ Note:
87
+ `primitives.Semaphore` is a subclass of `asyncio.Semaphore`. Therefore, when the documentation refers to `asyncio.Semaphore`, it also includes `primitives.Semaphore` and any other subclasses.
88
+ """
89
+
90
+
31
91
  def apply_semaphore(
32
92
  coro_fn: Optional[Union[CoroFn[P, T], SemaphoreSpec]] = None,
33
93
  semaphore: SemaphoreSpec = None,
34
94
  ) -> AsyncDecoratorOrCoroFn[P, T]:
95
+ """Apply a semaphore to a coroutine function or return a decorator.
96
+
97
+ This function can be used to apply a semaphore to a coroutine function either by
98
+ passing the coroutine function and semaphore as arguments or by using the semaphore
99
+ as a decorator. It raises exceptions if the inputs are not valid.
100
+
101
+ Args:
102
+ coro_fn (Optional[Callable]): The coroutine function to which the semaphore will be applied,
103
+ or None if the semaphore is to be used as a decorator.
104
+ semaphore (Union[int, asyncio.Semaphore, primitives.Semaphore]):
105
+ The semaphore to apply, which can be an integer, an `asyncio.Semaphore`, or a `primitives.Semaphore`.
106
+
107
+ Raises:
108
+ ValueError: If both `coro_fn` and `semaphore` are provided and the first argument is an integer or `asyncio.Semaphore`.
109
+ exceptions.FunctionNotAsync: If the provided function is not a coroutine.
110
+ TypeError: If the semaphore is not an integer, an `asyncio.Semaphore`, or a `primitives.Semaphore`.
111
+
112
+ Examples:
113
+ Using as a decorator:
114
+ >>> @apply_semaphore(2)
115
+ ... async def limited_concurrent_function():
116
+ ... pass
117
+
118
+ Applying directly to a function:
119
+ >>> async def my_coroutine():
120
+ ... pass
121
+ >>> my_coroutine = apply_semaphore(my_coroutine, 3)
122
+
123
+ Handling invalid inputs:
124
+ >>> try:
125
+ ... apply_semaphore(3, 2)
126
+ ... except ValueError as e:
127
+ ... print(e)
128
+
129
+ See Also:
130
+ - :class:`asyncio.Semaphore`
131
+ - :class:`primitives.Semaphore`
132
+
133
+ Note:
134
+ `primitives.Semaphore` is a subclass of `asyncio.Semaphore`. Therefore, when the documentation refers to `asyncio.Semaphore`, it also includes `primitives.Semaphore` and any other subclasses.
135
+ """
35
136
  # Parse Inputs
36
137
  if isinstance(coro_fn, (int, asyncio.Semaphore)):
37
138
  if semaphore is not None:
38
139
  raise ValueError("You can only pass in one arg.")
39
140
  semaphore = coro_fn
40
141
  coro_fn = None
41
-
142
+
42
143
  elif not asyncio.iscoroutinefunction(coro_fn):
43
144
  raise exceptions.FunctionNotAsync(coro_fn)
44
-
145
+
45
146
  # Create the semaphore if necessary
46
147
  if isinstance(semaphore, int):
47
148
  semaphore = primitives.ThreadsafeSemaphore(semaphore)
48
149
  elif not isinstance(semaphore, asyncio.Semaphore):
49
- raise TypeError(f"'semaphore' must either be an integer or a Semaphore object. You passed {semaphore}")
50
-
150
+ raise TypeError(
151
+ f"'semaphore' must either be an integer or a Semaphore object. You passed {semaphore}"
152
+ )
153
+
51
154
  # Create and return the decorator
52
155
  if isinstance(semaphore, primitives.Semaphore):
53
156
  # NOTE: Our `Semaphore` primitive can be used as a decorator.
54
157
  # While you can use it the `async with` way like any other semaphore and we could make this code section cleaner,
55
158
  # applying it as a decorator adds some useful info to its debug logs so we do that here if we can.
56
159
  semaphore_decorator = semaphore
57
-
160
+
58
161
  else:
162
+
59
163
  def semaphore_decorator(coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
60
164
  @functools.wraps(coro_fn)
61
165
  async def semaphore_wrap(*args, **kwargs) -> T:
62
166
  async with semaphore: # type: ignore [union-attr]
63
167
  return await coro_fn(*args, **kwargs)
168
+
64
169
  return semaphore_wrap
65
-
170
+
66
171
  return semaphore_decorator if coro_fn is None else semaphore_decorator(coro_fn)
67
172
 
68
173
 
69
174
  dummy_semaphore = primitives.DummySemaphore()
175
+ """A dummy semaphore that does not enforce any concurrency limits."""