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
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('kwargs', present_flags)
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
- Returns:
41
- True if the operation should be synchronous, False otherwise.
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, ModifiedMixin
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 ASyncPropertyDescriptor, ASyncCachedPropertyDescriptor
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
- """Any class with metaclass ASyncMeta will have its functions wrapped with a_sync upon class instantiation."""
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("woah, you're defining a new ASync class `%s`! let's walk thru it together", new_class_name)
24
- logger.debug("first, I check whether you've defined any modifiers on `%s`", new_class_name)
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
- logger.debug('found modifiers: %s', class_defined_modifiers)
30
- logger.debug("now I inspect the class definition to figure out which attributes need to be wrapped")
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("`%s.%s` starts with an underscore, skipping", new_class_name, attr_name)
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("`%s.%s` incluldes a double-underscore, skipping", new_class_name, attr_name)
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("`%s.%s` is a %s, skipping", new_class_name, attr_name, attr_value.__class__.__name__)
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(f"inspecting `{new_class_name}.{attr_name}` of type {attr_value.__class__.__name__}")
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, ModifiedMixin):
45
- logger.debug("`%s.%s` is a `ModifiedMixin` object, which means you decorated it with an a_sync decorator even though `%s` is an ASyncABC class", new_class_name, attr_name, new_class_name)
46
- logger.debug("you probably did this so you could apply some modifiers to `%s` specifically", attr_name)
47
- modified_modifiers = attr_value.modifiers._modifiers
48
- if modified_modifiers:
49
- logger.debug("I found `%s.%s` is modified with %s", new_class_name, attr_name, modified_modifiers)
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("full modifier set for `%s.%s`: %s", new_class_name, attr_name, fn_modifiers)
54
- if isinstance(attr_value, (ASyncPropertyDescriptor, ASyncCachedPropertyDescriptor)):
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("since `%s` is a property, we will add a hidden dundermethod so you can still access it both sync and async", attr_name)
58
- attrs[attr_value.hidden_method_name] = attr_value.hidden_method_descriptor
59
- logger.debug("`%s.%s` is now %s", new_class_name, attr_value.hidden_method_name, attr_value.hidden_method_descriptor)
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("`%s.%s` is not callable, we will take no action with it", new_class_name, attr_name)
71
- return super(ASyncMeta, cls).__new__(cls, new_class_name, bases, attrs)
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
- def __init__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> None:
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
- if ENVIRONMENT_VARIABLES.DEBUG_MODE or ENVIRONMENT_VARIABLES.DEBUG_CLASS_NAME == new_class_name:
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
- """Returns a boolean that indicates whether methods of 'instance' should be called as sync or async methods."""
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
- A flag indicating whether the instance should default to asynchronous execution.
33
-
34
- You can override this if you want.
35
- If you want to be able to hotswap instance modes, you can redefine this as a non-cached property.
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
- """You can override this if you want."""
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
- """You can override this if you want."""
48
- logger.debug("checking `%s.%s.__init__` signature against provided kwargs to determine a_sync mode for the new instance", cls.__module__, cls.__name__)
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("kwargs indicate the new instance created with args %s %s is %ssynchronous", args, kwargs, 'a' if sync is False else '')
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("No valid flags found in kwargs, checking class definition for defined default")
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
- """You should not override this."""
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
- @abc.abstractproperty
177
+ @property
178
+ @abc.abstractmethod
70
179
  def __a_sync_flag_name__(self) -> str:
71
- pass
72
-
73
- @abc.abstractproperty
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
- pass
76
-
77
- @abc.abstractclassmethod # type: ignore [arg-type, misc]
78
- def __a_sync_default_mode__(cls) -> bool: # type: ignore [empty-body]
79
- # mypy doesnt recognize this abc member
80
- pass
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(f"You should not create instances of `{cls_name}` directly, you should subclass `ASyncGenericBase` instead.")
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("unable to find flag name using `%s.__init__` signature, checking for flag attributes defined on %s", self.__class__.__name__, self)
71
- present_flags = [flag for flag in _flags.VIABLE_FLAGS if hasattr(self, flag)]
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('`%s.%s` is currently %s', self, flag, flag_value)
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("`%s.%s` indicates default mode is %ssynchronous", cls, flag, 'a' if sync is False else '')
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("There are no flags defined on the base class, this is expected. Skipping.")
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
- # idk why __dict__ doesn't type check as a dict
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
- # idk why __dict__ doesn't type check as a dict
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("checking `__init__` signature for default %s a_sync flag value", cls)
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('%s defines %s, default value %s', cls, flag, flag_value)
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