ez-a-sync 0.22.13__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.
- a_sync/ENVIRONMENT_VARIABLES.py +4 -3
- a_sync/__init__.py +30 -12
- a_sync/_smart.py +132 -28
- a_sync/_typing.py +56 -12
- a_sync/a_sync/__init__.py +35 -10
- a_sync/a_sync/_descriptor.py +74 -26
- a_sync/a_sync/_flags.py +14 -6
- a_sync/a_sync/_helpers.py +8 -7
- a_sync/a_sync/_kwargs.py +3 -2
- a_sync/a_sync/_meta.py +120 -28
- a_sync/a_sync/abstract.py +102 -28
- a_sync/a_sync/base.py +34 -16
- a_sync/a_sync/config.py +47 -13
- a_sync/a_sync/decorator.py +239 -117
- a_sync/a_sync/function.py +416 -146
- a_sync/a_sync/method.py +197 -59
- a_sync/a_sync/modifiers/__init__.py +47 -5
- a_sync/a_sync/modifiers/cache/__init__.py +46 -17
- a_sync/a_sync/modifiers/cache/memory.py +86 -20
- a_sync/a_sync/modifiers/limiter.py +52 -22
- a_sync/a_sync/modifiers/manager.py +98 -16
- a_sync/a_sync/modifiers/semaphores.py +48 -15
- a_sync/a_sync/property.py +383 -82
- a_sync/a_sync/singleton.py +1 -0
- a_sync/aliases.py +0 -1
- a_sync/asyncio/__init__.py +4 -1
- a_sync/asyncio/as_completed.py +177 -49
- a_sync/asyncio/create_task.py +31 -17
- a_sync/asyncio/gather.py +72 -52
- a_sync/asyncio/utils.py +3 -3
- a_sync/exceptions.py +78 -23
- a_sync/executor.py +120 -71
- a_sync/future.py +575 -158
- a_sync/iter.py +110 -50
- a_sync/primitives/__init__.py +14 -2
- a_sync/primitives/_debug.py +13 -13
- a_sync/primitives/_loggable.py +5 -4
- a_sync/primitives/locks/__init__.py +5 -2
- a_sync/primitives/locks/counter.py +38 -36
- a_sync/primitives/locks/event.py +21 -7
- a_sync/primitives/locks/prio_semaphore.py +182 -62
- a_sync/primitives/locks/semaphore.py +78 -77
- a_sync/primitives/queue.py +560 -58
- a_sync/sphinx/__init__.py +0 -1
- a_sync/sphinx/ext.py +160 -50
- a_sync/task.py +262 -97
- a_sync/utils/__init__.py +12 -6
- a_sync/utils/iterators.py +127 -43
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/METADATA +1 -1
- ez_a_sync-0.22.15.dist-info/RECORD +74 -0
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -2
- tests/executor.py +112 -9
- tests/fixtures.py +61 -32
- tests/test_abstract.py +7 -4
- tests/test_as_completed.py +54 -21
- tests/test_base.py +66 -17
- tests/test_cache.py +31 -15
- tests/test_decorator.py +54 -28
- tests/test_executor.py +8 -13
- tests/test_future.py +45 -8
- tests/test_gather.py +8 -2
- tests/test_helpers.py +2 -0
- tests/test_iter.py +55 -13
- tests/test_limiter.py +5 -3
- tests/test_meta.py +23 -9
- tests/test_modified.py +4 -1
- tests/test_semaphore.py +15 -8
- tests/test_singleton.py +15 -10
- tests/test_task.py +126 -28
- ez_a_sync-0.22.13.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/top_level.txt +0 -0
a_sync/a_sync/_descriptor.py
CHANGED
|
@@ -9,17 +9,20 @@ import functools
|
|
|
9
9
|
|
|
10
10
|
from a_sync._typing import *
|
|
11
11
|
from a_sync.a_sync import decorator
|
|
12
|
-
from a_sync.a_sync.function import ASyncFunction,
|
|
12
|
+
from a_sync.a_sync.function import ASyncFunction, ModifierManager, _ModifiedMixin
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from a_sync import TaskMapping
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
class ASyncDescriptor(_ModifiedMixin, Generic[I, P, T]):
|
|
18
19
|
"""
|
|
19
|
-
A descriptor base class for
|
|
20
|
+
A descriptor base class for dual-function ASync methods and properties.
|
|
20
21
|
|
|
21
22
|
This class provides functionality for mapping operations across multiple instances
|
|
22
|
-
and includes utility methods for common operations
|
|
23
|
+
and includes utility methods for common operations such as checking if all or any
|
|
24
|
+
results are truthy, and finding the minimum, maximum, or sum of results of the method
|
|
25
|
+
or property mapped across multiple instances.
|
|
23
26
|
"""
|
|
24
27
|
|
|
25
28
|
__wrapped__: AnyFn[Concatenate[I, P], T]
|
|
@@ -28,9 +31,9 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
28
31
|
__slots__ = "field_name", "_fget"
|
|
29
32
|
|
|
30
33
|
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
_fget: AnyFn[Concatenate[I, P], T],
|
|
33
|
-
field_name: Optional[str] = None,
|
|
34
|
+
self,
|
|
35
|
+
_fget: AnyFn[Concatenate[I, P], T],
|
|
36
|
+
field_name: Optional[str] = None,
|
|
34
37
|
**modifiers: ModifierKwargs,
|
|
35
38
|
) -> None:
|
|
36
39
|
"""
|
|
@@ -45,19 +48,21 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
45
48
|
ValueError: If _fget is not callable.
|
|
46
49
|
"""
|
|
47
50
|
if not callable(_fget):
|
|
48
|
-
raise ValueError(f
|
|
51
|
+
raise ValueError(f"Unable to decorate {_fget}")
|
|
49
52
|
self.modifiers = ModifierManager(modifiers)
|
|
50
53
|
if isinstance(_fget, ASyncFunction):
|
|
51
54
|
self.modifiers.update(_fget.modifiers)
|
|
52
55
|
self.__wrapped__ = _fget
|
|
53
56
|
elif asyncio.iscoroutinefunction(_fget):
|
|
54
|
-
self.__wrapped__: AsyncUnboundMethod[I, P, T] =
|
|
57
|
+
self.__wrapped__: AsyncUnboundMethod[I, P, T] = (
|
|
58
|
+
self.modifiers.apply_async_modifiers(_fget)
|
|
59
|
+
)
|
|
55
60
|
else:
|
|
56
61
|
self.__wrapped__ = _fget
|
|
57
62
|
|
|
58
63
|
self.field_name = field_name or _fget.__name__
|
|
59
64
|
"""The name of the field the {cls} is bound to."""
|
|
60
|
-
|
|
65
|
+
|
|
61
66
|
functools.update_wrapper(self, self.__wrapped__)
|
|
62
67
|
|
|
63
68
|
def __repr__(self) -> str:
|
|
@@ -73,7 +78,9 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
73
78
|
"""
|
|
74
79
|
self.field_name = name
|
|
75
80
|
|
|
76
|
-
def map(
|
|
81
|
+
def map(
|
|
82
|
+
self, *instances: AnyIterable[I], **bound_method_kwargs: P.kwargs
|
|
83
|
+
) -> "TaskMapping[I, T]":
|
|
77
84
|
"""
|
|
78
85
|
Create a TaskMapping for the given instances.
|
|
79
86
|
|
|
@@ -85,6 +92,7 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
85
92
|
A TaskMapping object.
|
|
86
93
|
"""
|
|
87
94
|
from a_sync.task import TaskMapping
|
|
95
|
+
|
|
88
96
|
return TaskMapping(self, *instances, **bound_method_kwargs)
|
|
89
97
|
|
|
90
98
|
@functools.cached_property
|
|
@@ -137,7 +145,13 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
137
145
|
"""
|
|
138
146
|
return decorator.a_sync(default=self.default)(self._sum)
|
|
139
147
|
|
|
140
|
-
async def _all(
|
|
148
|
+
async def _all(
|
|
149
|
+
self,
|
|
150
|
+
*instances: AnyIterable[I],
|
|
151
|
+
concurrency: Optional[int] = None,
|
|
152
|
+
name: str = "",
|
|
153
|
+
**kwargs: P.kwargs,
|
|
154
|
+
) -> bool:
|
|
141
155
|
"""
|
|
142
156
|
Check if all results are truthy.
|
|
143
157
|
|
|
@@ -150,9 +164,17 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
150
164
|
Returns:
|
|
151
165
|
A boolean indicating if all results are truthy.
|
|
152
166
|
"""
|
|
153
|
-
return await self.map(
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
return await self.map(
|
|
168
|
+
*instances, concurrency=concurrency, name=name, **kwargs
|
|
169
|
+
).all(pop=True, sync=False)
|
|
170
|
+
|
|
171
|
+
async def _any(
|
|
172
|
+
self,
|
|
173
|
+
*instances: AnyIterable[I],
|
|
174
|
+
concurrency: Optional[int] = None,
|
|
175
|
+
name: str = "",
|
|
176
|
+
**kwargs: P.kwargs,
|
|
177
|
+
) -> bool:
|
|
156
178
|
"""
|
|
157
179
|
Check if any result is truthy.
|
|
158
180
|
|
|
@@ -165,9 +187,17 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
165
187
|
Returns:
|
|
166
188
|
A boolean indicating if any result is truthy.
|
|
167
189
|
"""
|
|
168
|
-
return await self.map(
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
return await self.map(
|
|
191
|
+
*instances, concurrency=concurrency, name=name, **kwargs
|
|
192
|
+
).any(pop=True, sync=False)
|
|
193
|
+
|
|
194
|
+
async def _min(
|
|
195
|
+
self,
|
|
196
|
+
*instances: AnyIterable[I],
|
|
197
|
+
concurrency: Optional[int] = None,
|
|
198
|
+
name: str = "",
|
|
199
|
+
**kwargs: P.kwargs,
|
|
200
|
+
) -> T:
|
|
171
201
|
"""
|
|
172
202
|
Find the minimum result.
|
|
173
203
|
|
|
@@ -180,9 +210,17 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
180
210
|
Returns:
|
|
181
211
|
The minimum result.
|
|
182
212
|
"""
|
|
183
|
-
return await self.map(
|
|
184
|
-
|
|
185
|
-
|
|
213
|
+
return await self.map(
|
|
214
|
+
*instances, concurrency=concurrency, name=name, **kwargs
|
|
215
|
+
).min(pop=True, sync=False)
|
|
216
|
+
|
|
217
|
+
async def _max(
|
|
218
|
+
self,
|
|
219
|
+
*instances: AnyIterable[I],
|
|
220
|
+
concurrency: Optional[int] = None,
|
|
221
|
+
name: str = "",
|
|
222
|
+
**kwargs: P.kwargs,
|
|
223
|
+
) -> T:
|
|
186
224
|
"""
|
|
187
225
|
Find the maximum result.
|
|
188
226
|
|
|
@@ -195,9 +233,17 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
195
233
|
Returns:
|
|
196
234
|
The maximum result.
|
|
197
235
|
"""
|
|
198
|
-
return await self.map(
|
|
199
|
-
|
|
200
|
-
|
|
236
|
+
return await self.map(
|
|
237
|
+
*instances, concurrency=concurrency, name=name, **kwargs
|
|
238
|
+
).max(pop=True, sync=False)
|
|
239
|
+
|
|
240
|
+
async def _sum(
|
|
241
|
+
self,
|
|
242
|
+
*instances: AnyIterable[I],
|
|
243
|
+
concurrency: Optional[int] = None,
|
|
244
|
+
name: str = "",
|
|
245
|
+
**kwargs: P.kwargs,
|
|
246
|
+
) -> T:
|
|
201
247
|
"""
|
|
202
248
|
Calculate the sum of results.
|
|
203
249
|
|
|
@@ -210,10 +256,12 @@ class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
|
|
|
210
256
|
Returns:
|
|
211
257
|
The sum of the results.
|
|
212
258
|
"""
|
|
213
|
-
return await self.map(
|
|
259
|
+
return await self.map(
|
|
260
|
+
*instances, concurrency=concurrency, name=name, **kwargs
|
|
261
|
+
).sum(pop=True, sync=False)
|
|
214
262
|
|
|
215
263
|
def __init_subclass__(cls) -> None:
|
|
216
264
|
for attr in cls.__dict__.values():
|
|
217
265
|
if attr.__doc__ and "{cls}" in attr.__doc__:
|
|
218
266
|
attr.__doc__ = attr.__doc__.replace("{cls}", f":class:`{cls.__name__}`")
|
|
219
|
-
return super().__init_subclass__()
|
|
267
|
+
return super().__init_subclass__()
|
a_sync/a_sync/_flags.py
CHANGED
|
@@ -2,24 +2,31 @@
|
|
|
2
2
|
This module provides functionality for handling synchronous and asynchronous flags
|
|
3
3
|
in the ez-a-sync library.
|
|
4
4
|
|
|
5
|
-
ez-a-sync uses 'flags' to indicate whether objects
|
|
5
|
+
ez-a-sync uses 'flags' to indicate whether objects or function calls will be synchronous or asynchronous.
|
|
6
6
|
|
|
7
|
-
You can use any of the provided flags, whichever makes most sense for your use case.
|
|
7
|
+
You can use any of the provided flags, whichever makes the most sense for your use case.
|
|
8
|
+
|
|
9
|
+
AFFIRMATIVE_FLAGS: Set of flags indicating synchronous behavior. Currently includes "sync".
|
|
10
|
+
|
|
11
|
+
NEGATIVE_FLAGS: Set of flags indicating asynchronous behavior. Currently includes "asynchronous".
|
|
12
|
+
|
|
13
|
+
VIABLE_FLAGS: Set of all valid flags, combining both synchronous and asynchronous indicators.
|
|
8
14
|
"""
|
|
9
15
|
|
|
10
16
|
from typing import Any
|
|
11
17
|
|
|
12
18
|
from a_sync import exceptions
|
|
13
19
|
|
|
14
|
-
AFFIRMATIVE_FLAGS = {
|
|
20
|
+
AFFIRMATIVE_FLAGS = {"sync"}
|
|
15
21
|
"""Set of flags indicating synchronous behavior."""
|
|
16
22
|
|
|
17
|
-
NEGATIVE_FLAGS = {
|
|
23
|
+
NEGATIVE_FLAGS = {"asynchronous"}
|
|
18
24
|
"""Set of flags indicating asynchronous behavior."""
|
|
19
25
|
|
|
20
26
|
VIABLE_FLAGS = AFFIRMATIVE_FLAGS | NEGATIVE_FLAGS
|
|
21
27
|
"""Set of all valid flags."""
|
|
22
28
|
|
|
29
|
+
|
|
23
30
|
def negate_if_necessary(flag: str, flag_value: bool) -> bool:
|
|
24
31
|
"""Negate the flag value if necessary based on the flag type.
|
|
25
32
|
|
|
@@ -31,7 +38,7 @@ def negate_if_necessary(flag: str, flag_value: bool) -> bool:
|
|
|
31
38
|
The potentially negated flag value.
|
|
32
39
|
|
|
33
40
|
Raises:
|
|
34
|
-
|
|
41
|
+
exceptions.InvalidFlag: If the flag is not recognized.
|
|
35
42
|
"""
|
|
36
43
|
validate_flag_value(flag, flag_value)
|
|
37
44
|
if flag in AFFIRMATIVE_FLAGS:
|
|
@@ -40,6 +47,7 @@ def negate_if_necessary(flag: str, flag_value: bool) -> bool:
|
|
|
40
47
|
return bool(not flag_value)
|
|
41
48
|
raise exceptions.InvalidFlag(flag)
|
|
42
49
|
|
|
50
|
+
|
|
43
51
|
def validate_flag_value(flag: str, flag_value: Any) -> bool:
|
|
44
52
|
"""
|
|
45
53
|
Validate that the flag value is a boolean.
|
|
@@ -52,7 +60,7 @@ def validate_flag_value(flag: str, flag_value: Any) -> bool:
|
|
|
52
60
|
The validated flag value.
|
|
53
61
|
|
|
54
62
|
Raises:
|
|
55
|
-
|
|
63
|
+
exceptions.InvalidFlagValue: If the flag value is not a boolean.
|
|
56
64
|
"""
|
|
57
65
|
if not isinstance(flag_value, bool):
|
|
58
66
|
raise exceptions.InvalidFlagValue(flag, flag_value)
|
a_sync/a_sync/_helpers.py
CHANGED
|
@@ -19,11 +19,8 @@ def _await(awaitable: Awaitable[T]) -> T:
|
|
|
19
19
|
Args:
|
|
20
20
|
awaitable: The awaitable object to be awaited.
|
|
21
21
|
|
|
22
|
-
Returns:
|
|
23
|
-
The result of the awaitable.
|
|
24
|
-
|
|
25
22
|
Raises:
|
|
26
|
-
|
|
23
|
+
exceptions.SyncModeInAsyncContextError: If the event loop is already running.
|
|
27
24
|
"""
|
|
28
25
|
try:
|
|
29
26
|
return a_sync.asyncio.get_event_loop().run_until_complete(awaitable)
|
|
@@ -32,27 +29,31 @@ def _await(awaitable: Awaitable[T]) -> T:
|
|
|
32
29
|
raise exceptions.SyncModeInAsyncContextError from None
|
|
33
30
|
raise
|
|
34
31
|
|
|
32
|
+
|
|
35
33
|
def _asyncify(func: SyncFn[P, T], executor: Executor) -> CoroFn[P, T]: # type: ignore [misc]
|
|
36
34
|
"""
|
|
37
35
|
Convert a synchronous function to a coroutine function.
|
|
38
36
|
|
|
39
37
|
Args:
|
|
40
38
|
func: The synchronous function to be converted.
|
|
41
|
-
executor: The executor to run the synchronous function.
|
|
39
|
+
executor: The executor used to run the synchronous function.
|
|
42
40
|
|
|
43
41
|
Returns:
|
|
44
42
|
A coroutine function wrapping the input function.
|
|
45
43
|
|
|
46
44
|
Raises:
|
|
47
|
-
|
|
45
|
+
exceptions.FunctionNotSync: If the input function is a coroutine function or an instance of ASyncFunction.
|
|
48
46
|
"""
|
|
49
47
|
from a_sync.a_sync.function import ASyncFunction
|
|
48
|
+
|
|
50
49
|
if asyncio.iscoroutinefunction(func) or isinstance(func, ASyncFunction):
|
|
51
50
|
raise exceptions.FunctionNotSync(func)
|
|
51
|
+
|
|
52
52
|
@functools.wraps(func)
|
|
53
53
|
async def _asyncify_wrap(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
54
54
|
return await asyncio.futures.wrap_future(
|
|
55
|
-
executor.submit(func, *args, **kwargs),
|
|
55
|
+
executor.submit(func, *args, **kwargs),
|
|
56
56
|
loop=a_sync.asyncio.get_event_loop(),
|
|
57
57
|
)
|
|
58
|
+
|
|
58
59
|
return _asyncify_wrap
|
a_sync/a_sync/_kwargs.py
CHANGED
|
@@ -25,9 +25,10 @@ def get_flag_name(kwargs: dict) -> Optional[str]:
|
|
|
25
25
|
if len(present_flags) == 0:
|
|
26
26
|
return None
|
|
27
27
|
if len(present_flags) != 1:
|
|
28
|
-
raise exceptions.TooManyFlags(
|
|
28
|
+
raise exceptions.TooManyFlags("kwargs", present_flags)
|
|
29
29
|
return present_flags[0]
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
def is_sync(flag: str, kwargs: dict, pop_flag: bool = False) -> bool:
|
|
32
33
|
"""
|
|
33
34
|
Determine if the operation should be synchronous based on the flag value.
|
|
@@ -41,4 +42,4 @@ def is_sync(flag: str, kwargs: dict, pop_flag: bool = False) -> bool:
|
|
|
41
42
|
True if the operation should be synchronous, False otherwise.
|
|
42
43
|
"""
|
|
43
44
|
flag_value = kwargs.pop(flag) if pop_flag else kwargs[flag]
|
|
44
|
-
return _flags.negate_if_necessary(flag, flag_value)
|
|
45
|
+
return _flags.negate_if_necessary(flag, flag_value)
|
a_sync/a_sync/_meta.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import inspect
|
|
3
2
|
import logging
|
|
4
3
|
import threading
|
|
@@ -7,56 +6,124 @@ from typing import Any, Dict, Tuple
|
|
|
7
6
|
|
|
8
7
|
from a_sync import ENVIRONMENT_VARIABLES
|
|
9
8
|
from a_sync.a_sync import modifiers
|
|
10
|
-
from a_sync.a_sync.function import ASyncFunction,
|
|
9
|
+
from a_sync.a_sync.function import ASyncFunction, _ModifiedMixin
|
|
11
10
|
from a_sync.a_sync.method import ASyncMethodDescriptor
|
|
12
|
-
from a_sync.a_sync.property import
|
|
11
|
+
from a_sync.a_sync.property import (
|
|
12
|
+
ASyncCachedPropertyDescriptor,
|
|
13
|
+
ASyncPropertyDescriptor,
|
|
14
|
+
)
|
|
13
15
|
from a_sync.future import _ASyncFutureWrappedFn # type: ignore [attr-defined]
|
|
14
16
|
from a_sync.iter import ASyncGeneratorFunction
|
|
15
17
|
from a_sync.primitives.locks.semaphore import Semaphore
|
|
16
18
|
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
class ASyncMeta(ABCMeta):
|
|
20
|
-
"""
|
|
23
|
+
"""Metaclass for wrapping class attributes with asynchronous capabilities.
|
|
24
|
+
|
|
25
|
+
Any class with `ASyncMeta` as its metaclass will have its functions and properties
|
|
26
|
+
wrapped with asynchronous capabilities upon class instantiation. This includes
|
|
27
|
+
wrapping functions with `ASyncMethodDescriptor` and properties with
|
|
28
|
+
`ASyncPropertyDescriptor` or `ASyncCachedPropertyDescriptor`. Additionally, it handles
|
|
29
|
+
`_ModifiedMixin` objects (# TODO replace this with the actual subclasses of _modifiedMixin, which is just an internal use mixin class that has no meaning ot the user),
|
|
30
|
+
which are used when functions are decorated with a_sync decorators
|
|
31
|
+
to apply specific modifiers to those functions.
|
|
32
|
+
"""
|
|
33
|
+
|
|
21
34
|
def __new__(cls, new_class_name, bases, attrs):
|
|
22
35
|
_update_logger(new_class_name)
|
|
23
|
-
logger.debug(
|
|
24
|
-
|
|
36
|
+
logger.debug(
|
|
37
|
+
"woah, you're defining a new ASync class `%s`! let's walk thru it together",
|
|
38
|
+
new_class_name,
|
|
39
|
+
)
|
|
40
|
+
logger.debug(
|
|
41
|
+
"first, I check whether you've defined any modifiers on `%s`",
|
|
42
|
+
new_class_name,
|
|
43
|
+
)
|
|
25
44
|
# NOTE: Open quesion: what do we do when a parent class and subclass define the same modifier differently?
|
|
26
|
-
# Currently the parent value is used for functions defined on the parent,
|
|
45
|
+
# Currently the parent value is used for functions defined on the parent,
|
|
27
46
|
# and the subclass value is used for functions defined on the subclass.
|
|
28
47
|
class_defined_modifiers = modifiers.get_modifiers_from(attrs)
|
|
29
|
-
|
|
30
|
-
logger.debug("
|
|
48
|
+
|
|
49
|
+
logger.debug("found modifiers: %s", class_defined_modifiers)
|
|
50
|
+
logger.debug(
|
|
51
|
+
"now I inspect the class definition to figure out which attributes need to be wrapped"
|
|
52
|
+
)
|
|
31
53
|
for attr_name, attr_value in list(attrs.items()):
|
|
32
54
|
if attr_name.startswith("_"):
|
|
33
|
-
logger.debug(
|
|
55
|
+
logger.debug(
|
|
56
|
+
"`%s.%s` starts with an underscore, skipping",
|
|
57
|
+
new_class_name,
|
|
58
|
+
attr_name,
|
|
59
|
+
)
|
|
34
60
|
continue
|
|
35
61
|
elif "__" in attr_name:
|
|
36
|
-
logger.debug(
|
|
62
|
+
logger.debug(
|
|
63
|
+
"`%s.%s` incluldes a double-underscore, skipping",
|
|
64
|
+
new_class_name,
|
|
65
|
+
attr_name,
|
|
66
|
+
)
|
|
37
67
|
continue
|
|
38
68
|
elif isinstance(attr_value, (_ASyncFutureWrappedFn, Semaphore)):
|
|
39
|
-
logger.debug(
|
|
69
|
+
logger.debug(
|
|
70
|
+
"`%s.%s` is a %s, skipping",
|
|
71
|
+
new_class_name,
|
|
72
|
+
attr_name,
|
|
73
|
+
attr_value.__class__.__name__,
|
|
74
|
+
)
|
|
40
75
|
continue
|
|
41
|
-
logger.debug(
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"inspecting `{new_class_name}.{attr_name}` of type {attr_value.__class__.__name__}"
|
|
78
|
+
)
|
|
42
79
|
fn_modifiers = dict(class_defined_modifiers)
|
|
43
80
|
# Special handling for functions decorated with a_sync decorators
|
|
44
|
-
if isinstance(attr_value,
|
|
45
|
-
logger.debug(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
if isinstance(attr_value, _ModifiedMixin):
|
|
82
|
+
logger.debug(
|
|
83
|
+
"`%s.%s` is a `%s` object, which means you decorated it with an a_sync decorator even though `%s` is an ASyncABC class",
|
|
84
|
+
new_class_name,
|
|
85
|
+
attr_name,
|
|
86
|
+
type(attr_value).__name__,
|
|
87
|
+
new_class_name,
|
|
88
|
+
)
|
|
89
|
+
logger.debug(
|
|
90
|
+
"you probably did this so you could apply some modifiers to `%s` specifically",
|
|
91
|
+
attr_name,
|
|
92
|
+
)
|
|
93
|
+
if modified_modifiers := attr_value.modifiers._modifiers:
|
|
94
|
+
logger.debug(
|
|
95
|
+
"I found `%s.%s` is modified with %s",
|
|
96
|
+
new_class_name,
|
|
97
|
+
attr_name,
|
|
98
|
+
modified_modifiers,
|
|
99
|
+
)
|
|
50
100
|
fn_modifiers.update(modified_modifiers)
|
|
51
101
|
else:
|
|
52
102
|
logger.debug("I did not find any modifiers")
|
|
53
|
-
logger.debug(
|
|
54
|
-
|
|
103
|
+
logger.debug(
|
|
104
|
+
"full modifier set for `%s.%s`: %s",
|
|
105
|
+
new_class_name,
|
|
106
|
+
attr_name,
|
|
107
|
+
fn_modifiers,
|
|
108
|
+
)
|
|
109
|
+
if isinstance(
|
|
110
|
+
attr_value, (ASyncPropertyDescriptor, ASyncCachedPropertyDescriptor)
|
|
111
|
+
):
|
|
55
112
|
# Wrap property
|
|
56
113
|
logger.debug("`%s is a property, now let's wrap it", attr_name)
|
|
57
|
-
logger.debug(
|
|
58
|
-
|
|
59
|
-
|
|
114
|
+
logger.debug(
|
|
115
|
+
"since `%s` is a property, we will add a hidden dundermethod so you can still access it both sync and async",
|
|
116
|
+
attr_name,
|
|
117
|
+
)
|
|
118
|
+
attrs[attr_value.hidden_method_name] = (
|
|
119
|
+
attr_value.hidden_method_descriptor
|
|
120
|
+
)
|
|
121
|
+
logger.debug(
|
|
122
|
+
"`%s.%s` is now %s",
|
|
123
|
+
new_class_name,
|
|
124
|
+
attr_value.hidden_method_name,
|
|
125
|
+
attr_value.hidden_method_descriptor,
|
|
126
|
+
)
|
|
60
127
|
elif isinstance(attr_value, ASyncFunction):
|
|
61
128
|
attrs[attr_name] = ASyncMethodDescriptor(attr_value, **fn_modifiers)
|
|
62
129
|
else:
|
|
@@ -67,15 +134,30 @@ class ASyncMeta(ABCMeta):
|
|
|
67
134
|
# NOTE We will need to improve this logic if somebody needs to use it with classmethods or staticmethods.
|
|
68
135
|
attrs[attr_name] = ASyncMethodDescriptor(attr_value, **fn_modifiers)
|
|
69
136
|
else:
|
|
70
|
-
logger.debug(
|
|
71
|
-
|
|
137
|
+
logger.debug(
|
|
138
|
+
"`%s.%s` is not callable, we will take no action with it",
|
|
139
|
+
new_class_name,
|
|
140
|
+
attr_name,
|
|
141
|
+
)
|
|
142
|
+
return super(ASyncMeta, cls).__new__(cls, new_class_name, bases, attrs)
|
|
72
143
|
|
|
73
144
|
|
|
74
145
|
class ASyncSingletonMeta(ASyncMeta):
|
|
75
|
-
|
|
146
|
+
"""Metaclass for creating singleton instances with asynchronous capabilities.
|
|
147
|
+
|
|
148
|
+
This metaclass extends `ASyncMeta` to ensure that only one instance of a class
|
|
149
|
+
is created for each synchronous or asynchronous context.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]
|
|
154
|
+
) -> None:
|
|
76
155
|
cls.__instances: Dict[bool, object] = {}
|
|
156
|
+
"""Dictionary to store singleton instances."""
|
|
77
157
|
cls.__lock = threading.Lock()
|
|
158
|
+
"""Lock to ensure thread-safe instance creation."""
|
|
78
159
|
super().__init__(name, bases, namespace)
|
|
160
|
+
|
|
79
161
|
def __call__(cls, *args: Any, **kwargs: Any):
|
|
80
162
|
is_sync = cls.__a_sync_instance_will_be_sync__(args, kwargs) # type: ignore [attr-defined]
|
|
81
163
|
if is_sync not in cls.__instances:
|
|
@@ -85,8 +167,17 @@ class ASyncSingletonMeta(ASyncMeta):
|
|
|
85
167
|
cls.__instances[is_sync] = super().__call__(*args, **kwargs)
|
|
86
168
|
return cls.__instances[is_sync]
|
|
87
169
|
|
|
170
|
+
|
|
88
171
|
def _update_logger(new_class_name: str) -> None:
|
|
89
|
-
|
|
172
|
+
"""Update the logger configuration based on environment variables.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
new_class_name: The name of the new class being created.
|
|
176
|
+
"""
|
|
177
|
+
if (
|
|
178
|
+
ENVIRONMENT_VARIABLES.DEBUG_MODE
|
|
179
|
+
or ENVIRONMENT_VARIABLES.DEBUG_CLASS_NAME == new_class_name
|
|
180
|
+
):
|
|
90
181
|
logger.addHandler(_debug_handler)
|
|
91
182
|
logger.setLevel(logging.DEBUG)
|
|
92
183
|
logger.info("debug mode activated")
|
|
@@ -94,6 +185,7 @@ def _update_logger(new_class_name: str) -> None:
|
|
|
94
185
|
logger.removeHandler(_debug_handler)
|
|
95
186
|
logger.setLevel(logging.INFO)
|
|
96
187
|
|
|
188
|
+
|
|
97
189
|
_debug_handler = logging.StreamHandler()
|
|
98
190
|
|
|
99
191
|
__all__ = ["ASyncMeta", "ASyncSingletonMeta"]
|