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
@@ -12,28 +12,50 @@ from a_sync._typing import *
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
+
15
16
  def create_task(
16
- coro: Awaitable[T],
17
- *,
18
- name: Optional[str] = None,
17
+ coro: Awaitable[T],
18
+ *,
19
+ name: Optional[str] = None,
19
20
  skip_gc_until_done: bool = False,
20
21
  log_destroy_pending: bool = True,
21
22
  ) -> "asyncio.Task[T]":
22
23
  """
23
- Extends asyncio.create_task to support any Awaitable, manage task lifecycle, and enhance error handling.
24
+ Extends :func:`asyncio.create_task` to support any :class:`Awaitable`, manage task lifecycle, and enhance error handling.
25
+
26
+ This function accepts any :class:`Awaitable`, ensuring broader compatibility. If the Awaitable is not a coroutine,
27
+ it is awaited directly using a private helper function `__await`, which can handle any Awaitable object.
24
28
 
25
- Unlike asyncio.create_task, which requires a coroutine, this function accepts any Awaitable, ensuring broader
26
- compatibility. It optionally prevents the task from being garbage-collected until completion and provides
27
- enhanced error management by wrapping exceptions in a custom exception.
29
+ Note:
30
+ The `__await` function is designed to handle any Awaitable, implicitly managing non-coroutine Awaitables by awaiting them.
28
31
 
29
32
  Args:
30
- coro: An Awaitable object from which to create the task.
33
+ coro: An :class:`Awaitable` object from which to create the task.
31
34
  name: Optional name for the task, aiding in debugging.
32
35
  skip_gc_until_done: If True, the task is kept alive until it completes, preventing garbage collection.
36
+ Exceptions are wrapped in :class:`PersistedTaskException` for special handling within the
37
+ `__persisted_task_exc_wrap` function.
33
38
  log_destroy_pending: If False, asyncio's default error log when a pending task is destroyed is suppressed.
34
39
 
35
40
  Returns:
36
- An asyncio.Task object created from the provided Awaitable.
41
+ An :class:`asyncio.Task` object created from the provided Awaitable.
42
+
43
+ Examples:
44
+ Create a simple task with a coroutine:
45
+
46
+ >>> async def my_coroutine():
47
+ ... return "Hello, World!"
48
+ >>> task = create_task(my_coroutine())
49
+
50
+ Create a task with a non-coroutine Awaitable:
51
+
52
+ >>> from concurrent.futures import Future
53
+ >>> future = Future()
54
+ >>> task = create_task(future)
55
+
56
+ See Also:
57
+ - :func:`asyncio.create_task`
58
+ - :class:`asyncio.Task`
37
59
  """
38
60
 
39
61
  if not asyncio.iscoroutine(coro):
@@ -50,8 +72,26 @@ def create_task(
50
72
 
51
73
  __persisted_tasks: Set["asyncio.Task[Any]"] = set()
52
74
 
75
+
53
76
  async def __await(awaitable: Awaitable[T]) -> T:
54
- """Wait for the completion of an Awaitable."""
77
+ """Wait for the completion of an Awaitable.
78
+
79
+ Args:
80
+ awaitable: The :class:`Awaitable` object to wait for.
81
+
82
+ Raises:
83
+ RuntimeError: If a RuntimeError occurs during the await, it is raised with additional context.
84
+
85
+ Examples:
86
+ Await a simple coroutine:
87
+
88
+ >>> async def my_coroutine():
89
+ ... return "Hello, World!"
90
+ >>> result = await __await(my_coroutine())
91
+
92
+ See Also:
93
+ - :class:`Awaitable`
94
+ """
55
95
  try:
56
96
  return await awaitable
57
97
  except RuntimeError as e:
@@ -60,38 +100,48 @@ async def __await(awaitable: Awaitable[T]) -> T:
60
100
  args.append(awaitable._children)
61
101
  raise RuntimeError(*args) from None
62
102
 
103
+
63
104
  def __prune_persisted_tasks():
64
- """Remove completed tasks from the set of persisted tasks."""
105
+ """Remove completed tasks from the set of persisted tasks.
106
+
107
+ This function checks each task in the persisted tasks set. If a task is done and has an exception,
108
+ it logs the exception and raises it if it's not a :class:`PersistedTaskException`. It also logs the traceback
109
+ manually since the usual handler will not run after retrieving the exception.
110
+
111
+ See Also:
112
+ - :class:`PersistedTaskException`
113
+ """
65
114
  for task in tuple(__persisted_tasks):
66
115
  if task.done() and (e := task.exception()):
67
116
  # force exceptions related to this lib to bubble up
68
117
  if not isinstance(e, exceptions.PersistedTaskException):
69
118
  logger.exception(e)
70
119
  raise e
71
- # we have to manually log the traceback that asyncio would usually log
120
+ # we have to manually log the traceback that asyncio would usually log
72
121
  # since we already got the exception from the task and the usual handler will now not run
73
122
  context = {
74
- 'message': f'{task.__class__.__name__} exception was never retrieved',
75
- 'exception': e,
76
- 'future': task,
123
+ "message": f"{task.__class__.__name__} exception was never retrieved",
124
+ "exception": e,
125
+ "future": task,
77
126
  }
78
127
  if task._source_traceback:
79
- context['source_traceback'] = task._source_traceback
128
+ context["source_traceback"] = task._source_traceback
80
129
  task._loop.call_exception_handler(context)
81
130
  __persisted_tasks.discard(task)
82
131
 
132
+
83
133
  async def __persisted_task_exc_wrap(task: "asyncio.Task[T]") -> T:
84
134
  """
85
135
  Wrap a task to handle its exception in a specialized manner.
86
136
 
87
137
  Args:
88
- task: The asyncio Task to wrap.
89
-
90
- Returns:
91
- The result of the task, if successful.
138
+ task: The :class:`asyncio.Task` to wrap.
92
139
 
93
140
  Raises:
94
141
  PersistedTaskException: Wraps any exception raised by the task for special handling.
142
+
143
+ See Also:
144
+ - :class:`PersistedTaskException`
95
145
  """
96
146
  try:
97
147
  return await task
a_sync/asyncio/gather.py CHANGED
@@ -2,104 +2,163 @@
2
2
  This module provides an enhanced version of :func:`asyncio.gather`.
3
3
  """
4
4
 
5
- import asyncio
6
- from typing import (Any, Awaitable, Dict, List, Mapping, TypeVar, Union,
7
- overload)
5
+ from typing import Any, Awaitable, Dict, List, Mapping, Union, overload
6
+
7
+ from a_sync._typing import *
8
+ from a_sync.asyncio.as_completed import as_completed_mapping, _exc_wrap
8
9
 
9
10
  try:
10
11
  from tqdm.asyncio import tqdm_asyncio
11
12
  except ImportError as e:
13
+
12
14
  class tqdm_asyncio: # type: ignore [no-redef]
15
+ @staticmethod
13
16
  async def gather(*args, **kwargs):
14
- raise ImportError("You must have tqdm installed in order to use this feature")
15
-
16
- from a_sync._typing import *
17
- from a_sync.asyncio.as_completed import as_completed_mapping, _exc_wrap
17
+ raise ImportError(
18
+ "You must have tqdm installed in order to use this feature"
19
+ )
18
20
 
19
21
 
20
22
  Excluder = Callable[[T], bool]
21
23
 
24
+
22
25
  @overload
23
26
  async def gather(
24
- *awaitables: Mapping[K, Awaitable[V]],
25
- return_exceptions: bool = False,
26
- exclude_if: Optional[Excluder[V]] = None,
27
- tqdm: bool = False,
27
+ awaitables: Mapping[K, Awaitable[V]],
28
+ return_exceptions: bool = False,
29
+ exclude_if: Optional[Excluder[V]] = None,
30
+ tqdm: bool = False,
28
31
  **tqdm_kwargs: Any,
29
32
  ) -> Dict[K, V]:
30
- ...
33
+ """
34
+ Concurrently awaits a k:v mapping of awaitables and returns the results.
35
+
36
+ Args:
37
+ awaitables (Mapping[K, Awaitable[V]]): A mapping of keys to awaitable objects.
38
+ return_exceptions (bool, optional): If True, exceptions are returned as results instead of raising them. Defaults to False.
39
+ exclude_if (Optional[Excluder[V]], optional): A callable that takes a result and returns True if the result should be excluded from the final output. Defaults to None. Note: This is only applied when the input is not a mapping.
40
+ tqdm (bool, optional): If True, enables progress reporting using tqdm. Defaults to False.
41
+ **tqdm_kwargs: Additional keyword arguments for tqdm if progress reporting is enabled.
42
+
43
+ Examples:
44
+ Awaiting a mapping of awaitables:
45
+
46
+ >>> mapping = {'key1': thing1(), 'key2': thing2()}
47
+ >>> results = await gather(mapping)
48
+ >>> results
49
+ {'key1': 'result', 'key2': 123}
50
+
51
+ See Also:
52
+ :func:`asyncio.gather`
53
+ """
54
+
55
+
31
56
  @overload
32
57
  async def gather(
33
- *awaitables: Awaitable[T],
34
- return_exceptions: bool = False,
58
+ *awaitables: Awaitable[T],
59
+ return_exceptions: bool = False,
35
60
  exclude_if: Optional[Excluder[T]] = None,
36
61
  tqdm: bool = False,
37
62
  **tqdm_kwargs: Any,
38
63
  ) -> List[T]:
39
- ...
64
+ """
65
+ Concurrently awaits a series of awaitable objects and returns the results.
66
+
67
+ Args:
68
+ *awaitables (Awaitable[T]): The awaitables to await concurrently.
69
+ return_exceptions (bool, optional): If True, exceptions are returned as results instead of raising them. Defaults to False.
70
+ exclude_if (Optional[Excluder[T]], optional): A callable that takes a result and returns True if the result should be excluded from the final output. Defaults to None.
71
+ tqdm (bool, optional): If True, enables progress reporting using tqdm. Defaults to False.
72
+ **tqdm_kwargs: Additional keyword arguments for tqdm if progress reporting is enabled.
73
+
74
+ Examples:
75
+ Awaiting individual awaitables:
76
+
77
+ >>> results = await gather(thing1(), thing2())
78
+ >>> results
79
+ ['result', 123]
80
+
81
+ See Also:
82
+ :func:`asyncio.gather`
83
+ """
84
+
85
+
40
86
  async def gather(
41
- *awaitables: Union[Awaitable[T], Mapping[K, Awaitable[V]]],
42
- return_exceptions: bool = False,
43
- exclude_if: Optional[Excluder[T]] = None,
44
- tqdm: bool = False,
87
+ *awaitables: Union[Awaitable[T], Mapping[K, Awaitable[V]]],
88
+ return_exceptions: bool = False,
89
+ exclude_if: Optional[Excluder[T]] = None,
90
+ tqdm: bool = False,
45
91
  **tqdm_kwargs: Any,
46
92
  ) -> Union[List[T], Dict[K, V]]:
47
93
  """
48
- Concurrently awaits a list of awaitable objects or mappings of awaitables and returns the results.
94
+ Concurrently awaits a list of awaitable objects or a k:v mapping of awaitables, and returns the results.
49
95
 
50
- This function extends Python's asyncio.gather, providing additional features for mixed use cases of individual awaitable objects and mappings of awaitables.
96
+ This function extends Python's :func:`asyncio.gather`, providing additional features for handling either individual
97
+ awaitable objects or a single mapping of awaitables.
51
98
 
52
- Differences from asyncio.gather:
99
+ Differences from :func:`asyncio.gather`:
53
100
  - Uses type hints for use with static type checkers.
54
101
  - Supports gathering either individual awaitables or a k:v mapping of awaitables.
55
102
  - Provides progress reporting using tqdm if 'tqdm' is set to True.
56
-
103
+ - Allows exclusion of results based on a condition using the 'exclude_if' parameter. Note: This is only applied when the input is not a mapping.
104
+
57
105
  Args:
58
- *awaitables: The awaitables to await concurrently. It can be a single awaitable or a mapping of awaitables.
59
- return_exceptions (optional): If True, exceptions are returned as results instead of raising them. Defaults to False.
60
- tqdm (optional): If True, enables progress reporting using tqdm. Defaults to False.
106
+ *awaitables (Union[Awaitable[T], Mapping[K, Awaitable[V]]]): The awaitables to await concurrently. It can be a list of individual awaitables or a single mapping of awaitables.
107
+ return_exceptions (bool, optional): If True, exceptions are returned as results instead of raising them. Defaults to False.
108
+ exclude_if (Optional[Excluder[T]], optional): A callable that takes a result and returns True if the result should be excluded from the final output. Defaults to None. Note: This is only applied when the input is not a mapping.
109
+ tqdm (bool, optional): If True, enables progress reporting using tqdm. Defaults to False.
61
110
  **tqdm_kwargs: Additional keyword arguments for tqdm if progress reporting is enabled.
62
111
 
63
- Returns:
64
- A list of results when awaiting individual awaitables or a dictionary of results when awaiting mappings.
65
-
66
112
  Examples:
67
113
  Awaiting individual awaitables:
68
-
69
- - Results will be a list containing the result of each awaitable in sequential order.
70
114
 
71
- ```
72
115
  >>> results = await gather(thing1(), thing2())
73
116
  >>> results
74
117
  ['result', 123]
75
- ```
76
118
 
77
- Awaiting mappings of awaitables:
78
-
79
- - Results will be a dictionary with 'key1' mapped to the result of thing1() and 'key2' mapped to the result of thing2.
80
-
81
- ```
119
+ Awaiting a mapping of awaitables:
120
+
82
121
  >>> mapping = {'key1': thing1(), 'key2': thing2()}
83
122
  >>> results = await gather(mapping)
84
123
  >>> results
85
124
  {'key1': 'result', 'key2': 123}
86
- ```
125
+
126
+ See Also:
127
+ :func:`asyncio.gather`
87
128
  """
88
129
  is_mapping = _is_mapping(awaitables)
89
130
  results = await (
90
- gather_mapping(awaitables[0], return_exceptions=return_exceptions, exclude_if=exclude_if, tqdm=tqdm, **tqdm_kwargs) if is_mapping
91
- else tqdm_asyncio.gather(*(_exc_wrap(a) for a in awaitables) if return_exceptions else awaitables, **tqdm_kwargs) if tqdm
92
- else asyncio.gather(*awaitables, return_exceptions=return_exceptions) # type: ignore [arg-type]
131
+ gather_mapping(
132
+ awaitables[0],
133
+ return_exceptions=return_exceptions,
134
+ exclude_if=exclude_if,
135
+ tqdm=tqdm,
136
+ **tqdm_kwargs,
137
+ )
138
+ if is_mapping
139
+ else (
140
+ tqdm_asyncio.gather(
141
+ *(
142
+ (_exc_wrap(a) for a in awaitables)
143
+ if return_exceptions
144
+ else awaitables
145
+ ),
146
+ **tqdm_kwargs,
147
+ )
148
+ if tqdm
149
+ else asyncio.gather(*awaitables, return_exceptions=return_exceptions)
150
+ ) # type: ignore [arg-type]
93
151
  )
94
152
  if exclude_if and not is_mapping:
95
153
  results = [r for r in results if not exclude_if(r)]
96
154
  return results
97
-
155
+
156
+
98
157
  async def gather_mapping(
99
- mapping: Mapping[K, Awaitable[V]],
100
- return_exceptions: bool = False,
158
+ mapping: Mapping[K, Awaitable[V]],
159
+ return_exceptions: bool = False,
101
160
  exclude_if: Optional[Excluder[V]] = None,
102
- tqdm: bool = False,
161
+ tqdm: bool = False,
103
162
  **tqdm_kwargs: Any,
104
163
  ) -> Dict[K, V]:
105
164
  """
@@ -108,32 +167,40 @@ async def gather_mapping(
108
167
  This function is designed to await a mapping of awaitable objects, where each key-value pair represents a unique awaitable. It enables concurrent execution and gathers results into a dictionary.
109
168
 
110
169
  Args:
111
- mapping: A dictionary-like object where keys are of type K and values are awaitable objects of type V.
112
- return_exceptions (optional): If True, exceptions are returned as results instead of raising them. Defaults to False.
113
- tqdm (optional): If True, enables progress reporting using tqdm. Defaults to False.
170
+ mapping (Mapping[K, Awaitable[V]]): A dictionary-like object where keys are of type K and values are awaitable objects of type V.
171
+ return_exceptions (bool, optional): If True, exceptions are returned as results instead of raising them. Defaults to False.
172
+ exclude_if (Optional[Excluder[V]], optional): A callable that takes a result and returns True if the result should be excluded from the final output. Defaults to None. Note: This is not applied when the input is a mapping.
173
+ tqdm (bool, optional): If True, enables progress reporting using tqdm. Defaults to False.
114
174
  **tqdm_kwargs: Additional keyword arguments for tqdm if progress reporting is enabled.
115
175
 
116
- Returns:
117
- A dictionary with keys corresponding to the keys of the input mapping and values containing the results of the corresponding awaitables.
118
-
119
176
  Example:
120
177
  The 'results' dictionary will contain the awaited results, where keys match the keys in the 'mapping' and values contain the results of the corresponding awaitables.
121
- ```
178
+
122
179
  >>> mapping = {'task1': async_function1(), 'task2': async_function2(), 'task3': async_function3()}
123
180
  >>> results = await gather_mapping(mapping)
124
181
  >>> results
125
182
  {'task1': "result", 'task2': 123, 'task3': None}
126
- ```
183
+
184
+ See Also:
185
+ :func:`asyncio.gather`
127
186
  """
128
187
  results = {
129
- k: v
130
- async for k, v in as_completed_mapping(mapping, return_exceptions=return_exceptions, aiter=True, tqdm=tqdm, **tqdm_kwargs)
188
+ k: v
189
+ async for k, v in as_completed_mapping(
190
+ mapping,
191
+ return_exceptions=return_exceptions,
192
+ aiter=True,
193
+ tqdm=tqdm,
194
+ **tqdm_kwargs,
195
+ )
131
196
  if exclude_if is None or not exclude_if(v)
132
197
  }
133
198
  # return data in same order as input mapping
134
- return {k: results[k] for k in mapping}
199
+ return {k: results[k] for k in mapping}
135
200
 
136
201
 
137
- _is_mapping = lambda awaitables: len(awaitables) == 1 and isinstance(awaitables[0], Mapping)
202
+ _is_mapping = lambda awaitables: len(awaitables) == 1 and isinstance(
203
+ awaitables[0], Mapping
204
+ )
138
205
 
139
206
  __all__ = ["gather", "gather_mapping"]
a_sync/asyncio/utils.py CHANGED
@@ -1,12 +1,12 @@
1
-
2
1
  import asyncio
3
2
 
3
+
4
4
  def get_event_loop() -> asyncio.AbstractEventLoop:
5
5
  try:
6
6
  loop = asyncio.get_event_loop()
7
- except RuntimeError as e: # Necessary for use with multi-threaded applications.
7
+ except RuntimeError as e: # Necessary for use with multi-threaded applications.
8
8
  if not str(e).startswith("There is no current event loop in thread"):
9
9
  raise
10
10
  loop = asyncio.new_event_loop()
11
11
  asyncio.set_event_loop(loop)
12
- return loop
12
+ return loop