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.
- a_sync/ENVIRONMENT_VARIABLES.py +37 -5
- a_sync/__init__.py +53 -12
- a_sync/_smart.py +231 -28
- a_sync/_typing.py +112 -15
- a_sync/a_sync/__init__.py +35 -10
- a_sync/a_sync/_descriptor.py +248 -38
- a_sync/a_sync/_flags.py +78 -9
- a_sync/a_sync/_helpers.py +46 -13
- a_sync/a_sync/_kwargs.py +33 -8
- a_sync/a_sync/_meta.py +149 -28
- a_sync/a_sync/abstract.py +150 -28
- a_sync/a_sync/base.py +34 -16
- a_sync/a_sync/config.py +85 -14
- a_sync/a_sync/decorator.py +441 -139
- a_sync/a_sync/function.py +709 -147
- a_sync/a_sync/method.py +437 -110
- a_sync/a_sync/modifiers/__init__.py +85 -5
- a_sync/a_sync/modifiers/cache/__init__.py +116 -17
- a_sync/a_sync/modifiers/cache/memory.py +130 -20
- a_sync/a_sync/modifiers/limiter.py +101 -22
- a_sync/a_sync/modifiers/manager.py +142 -16
- a_sync/a_sync/modifiers/semaphores.py +121 -15
- a_sync/a_sync/property.py +383 -82
- a_sync/a_sync/singleton.py +44 -19
- a_sync/aliases.py +0 -1
- a_sync/asyncio/__init__.py +140 -1
- a_sync/asyncio/as_completed.py +213 -79
- a_sync/asyncio/create_task.py +70 -20
- a_sync/asyncio/gather.py +125 -58
- a_sync/asyncio/utils.py +3 -3
- a_sync/exceptions.py +248 -26
- a_sync/executor.py +164 -69
- a_sync/future.py +1227 -168
- a_sync/iter.py +173 -56
- a_sync/primitives/__init__.py +14 -2
- a_sync/primitives/_debug.py +72 -18
- a_sync/primitives/_loggable.py +41 -10
- a_sync/primitives/locks/__init__.py +5 -2
- a_sync/primitives/locks/counter.py +107 -38
- a_sync/primitives/locks/event.py +21 -7
- a_sync/primitives/locks/prio_semaphore.py +262 -63
- a_sync/primitives/locks/semaphore.py +138 -89
- a_sync/primitives/queue.py +601 -60
- a_sync/sphinx/__init__.py +0 -1
- a_sync/sphinx/ext.py +160 -50
- a_sync/task.py +313 -112
- a_sync/utils/__init__.py +12 -6
- a_sync/utils/iterators.py +170 -50
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/METADATA +1 -1
- ez_a_sync-0.22.16.dist-info/RECORD +74 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -2
- tests/executor.py +250 -9
- tests/fixtures.py +61 -32
- tests/test_abstract.py +22 -4
- tests/test_as_completed.py +54 -21
- tests/test_base.py +264 -19
- tests/test_cache.py +31 -15
- tests/test_decorator.py +54 -28
- tests/test_executor.py +31 -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 +28 -11
- tests/test_task.py +162 -36
- ez_a_sync-0.22.14.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/top_level.txt +0 -0
a_sync/a_sync/_kwargs.py
CHANGED
|
@@ -19,26 +19,51 @@ def get_flag_name(kwargs: dict) -> Optional[str]:
|
|
|
19
19
|
The name of the flag if present, None otherwise.
|
|
20
20
|
|
|
21
21
|
Raises:
|
|
22
|
-
:class:`exceptions.TooManyFlags`: If more than one flag is present in the kwargs
|
|
22
|
+
:class:`exceptions.TooManyFlags`: If more than one flag is present in the kwargs,
|
|
23
|
+
the exception includes the message "kwargs" and the list of present flags.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> get_flag_name({'sync': True})
|
|
27
|
+
'sync'
|
|
28
|
+
|
|
29
|
+
>>> get_flag_name({'async': False})
|
|
30
|
+
'async'
|
|
31
|
+
|
|
32
|
+
>>> get_flag_name({})
|
|
33
|
+
None
|
|
34
|
+
|
|
35
|
+
See Also:
|
|
36
|
+
:func:`is_sync`: Determines if the operation should be synchronous based on the flag value.
|
|
23
37
|
"""
|
|
24
38
|
present_flags = [flag for flag in _flags.VIABLE_FLAGS if flag in kwargs]
|
|
25
39
|
if len(present_flags) == 0:
|
|
26
40
|
return None
|
|
27
41
|
if len(present_flags) != 1:
|
|
28
|
-
raise exceptions.TooManyFlags(
|
|
42
|
+
raise exceptions.TooManyFlags("kwargs", present_flags)
|
|
29
43
|
return present_flags[0]
|
|
30
44
|
|
|
45
|
+
|
|
31
46
|
def is_sync(flag: str, kwargs: dict, pop_flag: bool = False) -> bool:
|
|
32
47
|
"""
|
|
33
48
|
Determine if the operation should be synchronous based on the flag value.
|
|
34
49
|
|
|
35
50
|
Args:
|
|
36
|
-
flag: The name of the flag to check.
|
|
37
|
-
kwargs: A dictionary of keyword arguments.
|
|
38
|
-
pop_flag: Whether to remove the flag from kwargs. Defaults to False.
|
|
51
|
+
flag (str): The name of the flag to check.
|
|
52
|
+
kwargs (dict): A dictionary of keyword arguments.
|
|
53
|
+
pop_flag (bool, optional): Whether to remove the flag from kwargs. Defaults to False.
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
Examples:
|
|
56
|
+
>>> is_sync('sync', {'sync': True})
|
|
57
|
+
True
|
|
58
|
+
|
|
59
|
+
>>> is_sync('async', {'async': False})
|
|
60
|
+
False
|
|
61
|
+
|
|
62
|
+
>>> is_sync('sync', {'sync': True}, pop_flag=True)
|
|
63
|
+
True
|
|
64
|
+
|
|
65
|
+
See Also:
|
|
66
|
+
:func:`get_flag_name`: Retrieves the name of the flag present in the kwargs.
|
|
42
67
|
"""
|
|
43
68
|
flag_value = kwargs.pop(flag) if pop_flag else kwargs[flag]
|
|
44
|
-
return _flags.negate_if_necessary(flag, flag_value)
|
|
69
|
+
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,141 @@ 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 :class:`~a_sync.a_sync.method.ASyncMethodDescriptor` and properties with
|
|
28
|
+
:class:`~a_sync.a_sync.property.ASyncPropertyDescriptor` or :class:`~a_sync.a_sync.property.ASyncCachedPropertyDescriptor`.
|
|
29
|
+
It also handles attributes that are instances of :class:`~a_sync.a_sync.function.ASyncFunction`,
|
|
30
|
+
which are used when functions are decorated with a_sync decorators to apply specific modifiers to those functions.
|
|
31
|
+
|
|
32
|
+
Attributes that are instances of :class:`~a_sync.future._ASyncFutureWrappedFn` and :class:`~a_sync.primitives.locks.semaphore.Semaphore`
|
|
33
|
+
are explicitly skipped and not wrapped.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
To create a class with asynchronous capabilities, define your class with `ASyncMeta` as its metaclass:
|
|
37
|
+
|
|
38
|
+
>>> class MyClass(metaclass=ASyncMeta):
|
|
39
|
+
... def my_method(self):
|
|
40
|
+
... return "Hello, World!"
|
|
41
|
+
|
|
42
|
+
The `my_method` function will be wrapped with :class:`~a_sync.a_sync.method.ASyncMethodDescriptor`, allowing it to be used asynchronously.
|
|
43
|
+
|
|
44
|
+
See Also:
|
|
45
|
+
- :class:`~a_sync.a_sync.function.ASyncFunction`
|
|
46
|
+
- :class:`~a_sync.a_sync.method.ASyncMethodDescriptor`
|
|
47
|
+
- :class:`~a_sync.a_sync.property.ASyncPropertyDescriptor`
|
|
48
|
+
- :class:`~a_sync.a_sync.property.ASyncCachedPropertyDescriptor`
|
|
49
|
+
"""
|
|
50
|
+
|
|
21
51
|
def __new__(cls, new_class_name, bases, attrs):
|
|
22
52
|
_update_logger(new_class_name)
|
|
23
|
-
logger.debug(
|
|
24
|
-
|
|
53
|
+
logger.debug(
|
|
54
|
+
"woah, you're defining a new ASync class `%s`! let's walk thru it together",
|
|
55
|
+
new_class_name,
|
|
56
|
+
)
|
|
57
|
+
logger.debug(
|
|
58
|
+
"first, I check whether you've defined any modifiers on `%s`",
|
|
59
|
+
new_class_name,
|
|
60
|
+
)
|
|
25
61
|
# 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,
|
|
62
|
+
# Currently the parent value is used for functions defined on the parent,
|
|
27
63
|
# and the subclass value is used for functions defined on the subclass.
|
|
28
64
|
class_defined_modifiers = modifiers.get_modifiers_from(attrs)
|
|
29
|
-
|
|
30
|
-
logger.debug("
|
|
65
|
+
|
|
66
|
+
logger.debug("found modifiers: %s", class_defined_modifiers)
|
|
67
|
+
logger.debug(
|
|
68
|
+
"now I inspect the class definition to figure out which attributes need to be wrapped"
|
|
69
|
+
)
|
|
31
70
|
for attr_name, attr_value in list(attrs.items()):
|
|
32
71
|
if attr_name.startswith("_"):
|
|
33
|
-
logger.debug(
|
|
72
|
+
logger.debug(
|
|
73
|
+
"`%s.%s` starts with an underscore, skipping",
|
|
74
|
+
new_class_name,
|
|
75
|
+
attr_name,
|
|
76
|
+
)
|
|
34
77
|
continue
|
|
35
78
|
elif "__" in attr_name:
|
|
36
|
-
logger.debug(
|
|
79
|
+
logger.debug(
|
|
80
|
+
"`%s.%s` incluldes a double-underscore, skipping",
|
|
81
|
+
new_class_name,
|
|
82
|
+
attr_name,
|
|
83
|
+
)
|
|
37
84
|
continue
|
|
38
85
|
elif isinstance(attr_value, (_ASyncFutureWrappedFn, Semaphore)):
|
|
39
|
-
logger.debug(
|
|
86
|
+
logger.debug(
|
|
87
|
+
"`%s.%s` is a %s, skipping",
|
|
88
|
+
new_class_name,
|
|
89
|
+
attr_name,
|
|
90
|
+
attr_value.__class__.__name__,
|
|
91
|
+
)
|
|
40
92
|
continue
|
|
41
|
-
logger.debug(
|
|
93
|
+
logger.debug(
|
|
94
|
+
f"inspecting `{new_class_name}.{attr_name}` of type {attr_value.__class__.__name__}"
|
|
95
|
+
)
|
|
42
96
|
fn_modifiers = dict(class_defined_modifiers)
|
|
43
97
|
# Special handling for functions decorated with a_sync decorators
|
|
44
|
-
if isinstance(attr_value,
|
|
45
|
-
logger.debug(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
if isinstance(attr_value, _ModifiedMixin):
|
|
99
|
+
logger.debug(
|
|
100
|
+
"`%s.%s` is a `%s` object, which means you decorated it with an a_sync decorator even though `%s` is an ASyncABC class",
|
|
101
|
+
new_class_name,
|
|
102
|
+
attr_name,
|
|
103
|
+
type(attr_value).__name__,
|
|
104
|
+
new_class_name,
|
|
105
|
+
)
|
|
106
|
+
logger.debug(
|
|
107
|
+
"you probably did this so you could apply some modifiers to `%s` specifically",
|
|
108
|
+
attr_name,
|
|
109
|
+
)
|
|
110
|
+
if modified_modifiers := attr_value.modifiers._modifiers:
|
|
111
|
+
logger.debug(
|
|
112
|
+
"I found `%s.%s` is modified with %s",
|
|
113
|
+
new_class_name,
|
|
114
|
+
attr_name,
|
|
115
|
+
modified_modifiers,
|
|
116
|
+
)
|
|
50
117
|
fn_modifiers.update(modified_modifiers)
|
|
51
118
|
else:
|
|
52
119
|
logger.debug("I did not find any modifiers")
|
|
53
|
-
logger.debug(
|
|
54
|
-
|
|
120
|
+
logger.debug(
|
|
121
|
+
"full modifier set for `%s.%s`: %s",
|
|
122
|
+
new_class_name,
|
|
123
|
+
attr_name,
|
|
124
|
+
fn_modifiers,
|
|
125
|
+
)
|
|
126
|
+
if isinstance(
|
|
127
|
+
attr_value, (ASyncPropertyDescriptor, ASyncCachedPropertyDescriptor)
|
|
128
|
+
):
|
|
55
129
|
# Wrap property
|
|
56
130
|
logger.debug("`%s is a property, now let's wrap it", attr_name)
|
|
57
|
-
logger.debug(
|
|
58
|
-
|
|
59
|
-
|
|
131
|
+
logger.debug(
|
|
132
|
+
"since `%s` is a property, we will add a hidden dundermethod so you can still access it both sync and async",
|
|
133
|
+
attr_name,
|
|
134
|
+
)
|
|
135
|
+
attrs[attr_value.hidden_method_name] = (
|
|
136
|
+
attr_value.hidden_method_descriptor
|
|
137
|
+
)
|
|
138
|
+
logger.debug(
|
|
139
|
+
"`%s.%s` is now %s",
|
|
140
|
+
new_class_name,
|
|
141
|
+
attr_value.hidden_method_name,
|
|
142
|
+
attr_value.hidden_method_descriptor,
|
|
143
|
+
)
|
|
60
144
|
elif isinstance(attr_value, ASyncFunction):
|
|
61
145
|
attrs[attr_name] = ASyncMethodDescriptor(attr_value, **fn_modifiers)
|
|
62
146
|
else:
|
|
@@ -67,15 +151,42 @@ class ASyncMeta(ABCMeta):
|
|
|
67
151
|
# NOTE We will need to improve this logic if somebody needs to use it with classmethods or staticmethods.
|
|
68
152
|
attrs[attr_name] = ASyncMethodDescriptor(attr_value, **fn_modifiers)
|
|
69
153
|
else:
|
|
70
|
-
logger.debug(
|
|
71
|
-
|
|
154
|
+
logger.debug(
|
|
155
|
+
"`%s.%s` is not callable, we will take no action with it",
|
|
156
|
+
new_class_name,
|
|
157
|
+
attr_name,
|
|
158
|
+
)
|
|
159
|
+
return super(ASyncMeta, cls).__new__(cls, new_class_name, bases, attrs)
|
|
72
160
|
|
|
73
161
|
|
|
74
162
|
class ASyncSingletonMeta(ASyncMeta):
|
|
75
|
-
|
|
163
|
+
"""Metaclass for creating singleton instances with asynchronous capabilities.
|
|
164
|
+
|
|
165
|
+
This metaclass extends :class:`~a_sync.a_sync._meta.ASyncMeta` to ensure that only one instance of a class
|
|
166
|
+
is created for each synchronous or asynchronous context.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
To create a singleton class with asynchronous capabilities, define your class with `ASyncSingletonMeta` as its metaclass:
|
|
170
|
+
|
|
171
|
+
>>> class MySingleton(metaclass=ASyncSingletonMeta):
|
|
172
|
+
... def __init__(self):
|
|
173
|
+
... print("Instance created")
|
|
174
|
+
|
|
175
|
+
The `MySingleton` class will ensure that only one instance is created for each context.
|
|
176
|
+
|
|
177
|
+
See Also:
|
|
178
|
+
- :class:`~a_sync.a_sync._meta.ASyncMeta`
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]
|
|
183
|
+
) -> None:
|
|
76
184
|
cls.__instances: Dict[bool, object] = {}
|
|
185
|
+
"""Dictionary to store singleton instances."""
|
|
77
186
|
cls.__lock = threading.Lock()
|
|
187
|
+
"""Lock to ensure thread-safe instance creation."""
|
|
78
188
|
super().__init__(name, bases, namespace)
|
|
189
|
+
|
|
79
190
|
def __call__(cls, *args: Any, **kwargs: Any):
|
|
80
191
|
is_sync = cls.__a_sync_instance_will_be_sync__(args, kwargs) # type: ignore [attr-defined]
|
|
81
192
|
if is_sync not in cls.__instances:
|
|
@@ -85,8 +196,17 @@ class ASyncSingletonMeta(ASyncMeta):
|
|
|
85
196
|
cls.__instances[is_sync] = super().__call__(*args, **kwargs)
|
|
86
197
|
return cls.__instances[is_sync]
|
|
87
198
|
|
|
199
|
+
|
|
88
200
|
def _update_logger(new_class_name: str) -> None:
|
|
89
|
-
|
|
201
|
+
"""Update the logger configuration based on environment variables.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
new_class_name: The name of the new class being created.
|
|
205
|
+
"""
|
|
206
|
+
if (
|
|
207
|
+
ENVIRONMENT_VARIABLES.DEBUG_MODE
|
|
208
|
+
or ENVIRONMENT_VARIABLES.DEBUG_CLASS_NAME == new_class_name
|
|
209
|
+
):
|
|
90
210
|
logger.addHandler(_debug_handler)
|
|
91
211
|
logger.setLevel(logging.DEBUG)
|
|
92
212
|
logger.info("debug mode activated")
|
|
@@ -94,6 +214,7 @@ def _update_logger(new_class_name: str) -> None:
|
|
|
94
214
|
logger.removeHandler(_debug_handler)
|
|
95
215
|
logger.setLevel(logging.INFO)
|
|
96
216
|
|
|
217
|
+
|
|
97
218
|
_debug_handler = logging.StreamHandler()
|
|
98
219
|
|
|
99
220
|
__all__ = ["ASyncMeta", "ASyncSingletonMeta"]
|
a_sync/a_sync/abstract.py
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides an abstract base class for defining asynchronous and synchronous behavior.
|
|
3
|
+
|
|
4
|
+
The :class:`ASyncABC` class uses the :class:`ASyncMeta` metaclass to facilitate the creation of classes
|
|
5
|
+
that can operate in both asynchronous and synchronous contexts. It provides concrete methods to determine
|
|
6
|
+
the execution mode based on flags and keyword arguments.
|
|
7
|
+
|
|
8
|
+
Note: It is recommended to use :class:`ASyncGenericBase` for most use cases. This class
|
|
9
|
+
is intended for more custom implementations if necessary.
|
|
10
|
+
"""
|
|
1
11
|
|
|
2
12
|
import abc
|
|
3
13
|
import functools
|
|
@@ -11,70 +21,182 @@ from a_sync.exceptions import NoFlagsFound
|
|
|
11
21
|
|
|
12
22
|
logger = logging.getLogger(__name__)
|
|
13
23
|
|
|
24
|
+
|
|
14
25
|
class ASyncABC(metaclass=ASyncMeta):
|
|
26
|
+
"""Abstract Base Class for defining asynchronous and synchronous behavior.
|
|
27
|
+
|
|
28
|
+
This class provides methods to determine the execution mode based on flags and keyword arguments.
|
|
29
|
+
It is designed to be subclassed, allowing developers to create classes that can be used in both
|
|
30
|
+
synchronous and asynchronous contexts.
|
|
31
|
+
|
|
32
|
+
See Also:
|
|
33
|
+
- :class:`ASyncGenericBase`: A more user-friendly base class for creating dual-mode classes.
|
|
34
|
+
- :class:`ASyncMeta`: Metaclass that facilitates asynchronous capabilities in class attributes.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
To create a class that inherits from `ASyncABC`, you need to implement the abstract methods
|
|
38
|
+
and can override the concrete methods if needed.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
class MyASyncClass(ASyncABC):
|
|
42
|
+
@property
|
|
43
|
+
def __a_sync_flag_name__(self) -> str:
|
|
44
|
+
return "sync"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def __a_sync_flag_value__(self) -> bool:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def __a_sync_default_mode__(cls) -> bool:
|
|
52
|
+
return False
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
In this example, `MyASyncClass` is a subclass of `ASyncABC` with custom implementations
|
|
56
|
+
for the required abstract methods.
|
|
57
|
+
"""
|
|
15
58
|
|
|
16
59
|
##################################
|
|
17
60
|
# Concrete Methods (overridable) #
|
|
18
61
|
##################################
|
|
19
62
|
|
|
20
63
|
def __a_sync_should_await__(self, kwargs: dict) -> bool:
|
|
21
|
-
"""
|
|
64
|
+
"""Determines if methods should be called asynchronously.
|
|
65
|
+
|
|
66
|
+
This method first checks the provided keyword arguments for flags
|
|
67
|
+
indicating the desired execution mode. If no flags are found, it
|
|
68
|
+
defaults to the instance's asynchronous flag.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
kwargs: A dictionary of keyword arguments to check for flags.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> instance = MyASyncClass()
|
|
75
|
+
>>> instance.__a_sync_should_await__({'sync': True})
|
|
76
|
+
False
|
|
77
|
+
"""
|
|
22
78
|
try:
|
|
23
|
-
# Defer to kwargs always
|
|
24
79
|
return self.__a_sync_should_await_from_kwargs__(kwargs)
|
|
25
80
|
except exceptions.NoFlagsFound:
|
|
26
|
-
# No flag found in kwargs, check for a flag attribute.
|
|
27
81
|
return self.__a_sync_instance_should_await__
|
|
28
82
|
|
|
29
83
|
@functools.cached_property
|
|
30
84
|
def __a_sync_instance_should_await__(self) -> bool:
|
|
85
|
+
"""Indicates if the instance should default to asynchronous execution.
|
|
86
|
+
|
|
87
|
+
This property can be overridden if dynamic behavior is needed. For
|
|
88
|
+
instance, to allow hot-swapping of instance modes, redefine this as a
|
|
89
|
+
non-cached property.
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
>>> instance = MyASyncClass()
|
|
93
|
+
>>> instance.__a_sync_instance_should_await__
|
|
94
|
+
True
|
|
31
95
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"""
|
|
37
|
-
return _flags.negate_if_necessary(self.__a_sync_flag_name__, self.__a_sync_flag_value__)
|
|
38
|
-
|
|
96
|
+
return _flags.negate_if_necessary(
|
|
97
|
+
self.__a_sync_flag_name__, self.__a_sync_flag_value__
|
|
98
|
+
)
|
|
99
|
+
|
|
39
100
|
def __a_sync_should_await_from_kwargs__(self, kwargs: dict) -> bool:
|
|
40
|
-
"""
|
|
101
|
+
"""Determines execution mode from keyword arguments.
|
|
102
|
+
|
|
103
|
+
This method can be overridden to customize how flags are extracted
|
|
104
|
+
from keyword arguments.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
kwargs: A dictionary of keyword arguments to check for flags.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
NoFlagsFound: If no valid flags are found in the keyword arguments.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> instance = MyASyncClass()
|
|
114
|
+
>>> instance.__a_sync_should_await_from_kwargs__({'sync': False})
|
|
115
|
+
True
|
|
116
|
+
"""
|
|
41
117
|
if flag := _kwargs.get_flag_name(kwargs):
|
|
42
118
|
return _kwargs.is_sync(flag, kwargs, pop_flag=True) # type: ignore [arg-type]
|
|
43
119
|
raise NoFlagsFound("kwargs", kwargs.keys())
|
|
44
|
-
|
|
120
|
+
|
|
45
121
|
@classmethod
|
|
46
122
|
def __a_sync_instance_will_be_sync__(cls, args: tuple, kwargs: dict) -> bool:
|
|
47
|
-
"""
|
|
48
|
-
|
|
123
|
+
"""Determines if a new instance will be synchronous.
|
|
124
|
+
|
|
125
|
+
This method checks the constructor's signature against provided
|
|
126
|
+
keyword arguments to determine the execution mode for the new instance.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
args: A tuple of positional arguments for the instance.
|
|
130
|
+
kwargs: A dictionary of keyword arguments for the instance.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
>>> MyASyncClass.__a_sync_instance_will_be_sync__((), {'sync': True})
|
|
134
|
+
True
|
|
135
|
+
"""
|
|
136
|
+
logger.debug(
|
|
137
|
+
"checking `%s.%s.__init__` signature against provided kwargs to determine a_sync mode for the new instance",
|
|
138
|
+
cls.__module__,
|
|
139
|
+
cls.__name__,
|
|
140
|
+
)
|
|
49
141
|
if flag := _kwargs.get_flag_name(kwargs):
|
|
50
142
|
sync = _kwargs.is_sync(flag, kwargs) # type: ignore [arg-type]
|
|
51
|
-
logger.debug(
|
|
143
|
+
logger.debug(
|
|
144
|
+
"kwargs indicate the new instance created with args %s %s is %ssynchronous",
|
|
145
|
+
args,
|
|
146
|
+
kwargs,
|
|
147
|
+
"a" if sync is False else "",
|
|
148
|
+
)
|
|
52
149
|
return sync
|
|
53
|
-
logger.debug(
|
|
150
|
+
logger.debug(
|
|
151
|
+
"No valid flags found in kwargs, checking class definition for defined default"
|
|
152
|
+
)
|
|
54
153
|
return cls.__a_sync_default_mode__() # type: ignore [arg-type]
|
|
55
154
|
|
|
56
155
|
######################################
|
|
57
156
|
# Concrete Methods (non-overridable) #
|
|
58
157
|
######################################
|
|
59
|
-
|
|
158
|
+
|
|
60
159
|
@property
|
|
61
160
|
def __a_sync_modifiers__(self: "ASyncABC") -> ModifierKwargs:
|
|
62
|
-
"""
|
|
161
|
+
"""Retrieves modifiers for the instance.
|
|
162
|
+
|
|
163
|
+
This method should not be overridden. It returns the modifiers
|
|
164
|
+
associated with the instance, which are used to customize behavior.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
>>> instance = MyASyncClass()
|
|
168
|
+
>>> instance.__a_sync_modifiers__
|
|
169
|
+
{'cache_type': 'memory'}
|
|
170
|
+
"""
|
|
63
171
|
return modifiers.get_modifiers_from(self)
|
|
64
172
|
|
|
65
173
|
####################
|
|
66
174
|
# Abstract Methods #
|
|
67
175
|
####################
|
|
68
176
|
|
|
69
|
-
@
|
|
177
|
+
@property
|
|
178
|
+
@abc.abstractmethod
|
|
70
179
|
def __a_sync_flag_name__(self) -> str:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
180
|
+
"""Abstract property for the flag name.
|
|
181
|
+
|
|
182
|
+
Subclasses must implement this property to return the name of the flag
|
|
183
|
+
used to determine execution mode.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
@abc.abstractmethod
|
|
74
188
|
def __a_sync_flag_value__(self) -> bool:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
189
|
+
"""Abstract property for the flag value.
|
|
190
|
+
|
|
191
|
+
Subclasses must implement this property to return the value of the flag
|
|
192
|
+
indicating the default execution mode.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
@abc.abstractmethod # type: ignore [arg-type, misc]
|
|
197
|
+
def __a_sync_default_mode__(cls) -> bool: # type: ignore [empty-body]
|
|
198
|
+
"""Abstract class method for the default execution mode.
|
|
199
|
+
|
|
200
|
+
Subclasses must implement this method to return the default execution
|
|
201
|
+
mode (synchronous or asynchronous) for instances of the class.
|
|
202
|
+
"""
|
a_sync/a_sync/base.py
CHANGED
|
@@ -11,6 +11,7 @@ from a_sync.a_sync.abstract import ASyncABC
|
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
class ASyncGenericBase(ASyncABC):
|
|
15
16
|
"""
|
|
16
17
|
Base class for creating dual-function sync/async-capable classes without writing all your code twice.
|
|
@@ -35,7 +36,7 @@ class ASyncGenericBase(ASyncABC):
|
|
|
35
36
|
@a_sync
|
|
36
37
|
async def my_method(self):
|
|
37
38
|
return await another_async_operation()
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
# Synchronous usage
|
|
40
41
|
obj = MyClass(sync=True)
|
|
41
42
|
sync_result = obj.my_property
|
|
@@ -56,8 +57,10 @@ class ASyncGenericBase(ASyncABC):
|
|
|
56
57
|
def __init__(self):
|
|
57
58
|
if type(self) is ASyncGenericBase:
|
|
58
59
|
cls_name = type(self).__name__
|
|
59
|
-
raise NotImplementedError(
|
|
60
|
-
|
|
60
|
+
raise NotImplementedError(
|
|
61
|
+
f"You should not create instances of `{cls_name}` directly, you should subclass `ASyncGenericBase` instead."
|
|
62
|
+
)
|
|
63
|
+
|
|
61
64
|
@functools.cached_property
|
|
62
65
|
def __a_sync_flag_name__(self) -> str:
|
|
63
66
|
logger.debug("checking a_sync flag for %s", self)
|
|
@@ -67,8 +70,14 @@ class ASyncGenericBase(ASyncABC):
|
|
|
67
70
|
# We can't get the flag name from the __init__ signature,
|
|
68
71
|
# but maybe the implementation sets the flag somewhere else.
|
|
69
72
|
# Let's check the instance's atributes
|
|
70
|
-
logger.debug(
|
|
71
|
-
|
|
73
|
+
logger.debug(
|
|
74
|
+
"unable to find flag name using `%s.__init__` signature, checking for flag attributes defined on %s",
|
|
75
|
+
self.__class__.__name__,
|
|
76
|
+
self,
|
|
77
|
+
)
|
|
78
|
+
present_flags = [
|
|
79
|
+
flag for flag in _flags.VIABLE_FLAGS if hasattr(self, flag)
|
|
80
|
+
]
|
|
72
81
|
if not present_flags:
|
|
73
82
|
raise exceptions.NoFlagsFound(self) from None
|
|
74
83
|
if len(present_flags) > 1:
|
|
@@ -77,7 +86,7 @@ class ASyncGenericBase(ASyncABC):
|
|
|
77
86
|
if not isinstance(flag, str):
|
|
78
87
|
raise exceptions.InvalidFlag(flag)
|
|
79
88
|
return flag
|
|
80
|
-
|
|
89
|
+
|
|
81
90
|
@functools.cached_property
|
|
82
91
|
def __a_sync_flag_value__(self) -> bool:
|
|
83
92
|
"""If you wish to be able to hotswap default modes, just duplicate this def as a non-cached property."""
|
|
@@ -85,7 +94,7 @@ class ASyncGenericBase(ASyncABC):
|
|
|
85
94
|
flag_value = getattr(self, flag)
|
|
86
95
|
if not isinstance(flag_value, bool):
|
|
87
96
|
raise exceptions.InvalidFlagValue(flag, flag_value)
|
|
88
|
-
logger.debug(
|
|
97
|
+
logger.debug("`%s.%s` is currently %s", self, flag, flag_value)
|
|
89
98
|
return flag_value
|
|
90
99
|
|
|
91
100
|
@classmethod # type: ignore [misc]
|
|
@@ -97,14 +106,21 @@ class ASyncGenericBase(ASyncABC):
|
|
|
97
106
|
flag = cls.__get_a_sync_flag_name_from_class_def()
|
|
98
107
|
flag_value = cls.__get_a_sync_flag_value_from_class_def(flag)
|
|
99
108
|
sync = _flags.negate_if_necessary(flag, flag_value) # type: ignore [arg-type]
|
|
100
|
-
logger.debug(
|
|
109
|
+
logger.debug(
|
|
110
|
+
"`%s.%s` indicates default mode is %ssynchronous",
|
|
111
|
+
cls,
|
|
112
|
+
flag,
|
|
113
|
+
"a" if sync is False else "",
|
|
114
|
+
)
|
|
101
115
|
return sync
|
|
102
|
-
|
|
116
|
+
|
|
103
117
|
@classmethod
|
|
104
118
|
def __get_a_sync_flag_name_from_signature(cls) -> Optional[str]:
|
|
105
119
|
logger.debug("Searching for flags defined on %s.__init__", cls)
|
|
106
120
|
if cls.__name__ == "ASyncGenericBase":
|
|
107
|
-
logger.debug(
|
|
121
|
+
logger.debug(
|
|
122
|
+
"There are no flags defined on the base class, this is expected. Skipping."
|
|
123
|
+
)
|
|
108
124
|
return None
|
|
109
125
|
parameters = inspect.signature(cls.__init__).parameters
|
|
110
126
|
logger.debug("parameters: %s", parameters)
|
|
@@ -115,17 +131,19 @@ class ASyncGenericBase(ASyncABC):
|
|
|
115
131
|
logger.debug("Searching for flags defined on %s", cls)
|
|
116
132
|
try:
|
|
117
133
|
return cls.__parse_flag_name_from_list(cls.__dict__) # type: ignore [arg-type]
|
|
118
|
-
|
|
134
|
+
# idk why __dict__ doesn't type check as a dict
|
|
119
135
|
except exceptions.NoFlagsFound:
|
|
120
136
|
for base in cls.__bases__:
|
|
121
137
|
with suppress(exceptions.NoFlagsFound):
|
|
122
|
-
return cls.__parse_flag_name_from_list(base.__dict__) # type: ignore [arg-type]
|
|
123
|
-
|
|
138
|
+
return cls.__parse_flag_name_from_list(base.__dict__) # type: ignore [arg-type]
|
|
139
|
+
# idk why __dict__ doesn't type check as a dict
|
|
124
140
|
raise exceptions.NoFlagsFound(cls, list(cls.__dict__.keys()))
|
|
125
141
|
|
|
126
142
|
@classmethod # type: ignore [misc]
|
|
127
143
|
def __a_sync_flag_default_value_from_signature(cls) -> bool:
|
|
128
|
-
logger.debug(
|
|
144
|
+
logger.debug(
|
|
145
|
+
"checking `__init__` signature for default %s a_sync flag value", cls
|
|
146
|
+
)
|
|
129
147
|
signature = inspect.signature(cls.__init__)
|
|
130
148
|
flag = cls.__get_a_sync_flag_name_from_signature()
|
|
131
149
|
flag_value = signature.parameters[flag].default
|
|
@@ -133,7 +151,7 @@ class ASyncGenericBase(ASyncABC):
|
|
|
133
151
|
raise NotImplementedError(
|
|
134
152
|
"The implementation for 'cls' uses an arg to specify sync mode, instead of a kwarg. We are unable to proceed. I suppose we can extend the code to accept positional arg flags if necessary"
|
|
135
153
|
)
|
|
136
|
-
logger.debug(
|
|
154
|
+
logger.debug("%s defines %s, default value %s", cls, flag, flag_value)
|
|
137
155
|
return flag_value
|
|
138
156
|
|
|
139
157
|
@classmethod
|
|
@@ -154,4 +172,4 @@ class ASyncGenericBase(ASyncABC):
|
|
|
154
172
|
raise exceptions.TooManyFlags(cls, present_flags)
|
|
155
173
|
flag = present_flags[0]
|
|
156
174
|
logger.debug("found flag %s", flag)
|
|
157
|
-
return flag
|
|
175
|
+
return flag
|