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.

Files changed (73) hide show
  1. a_sync/ENVIRONMENT_VARIABLES.py +4 -3
  2. a_sync/__init__.py +30 -12
  3. a_sync/_smart.py +132 -28
  4. a_sync/_typing.py +56 -12
  5. a_sync/a_sync/__init__.py +35 -10
  6. a_sync/a_sync/_descriptor.py +74 -26
  7. a_sync/a_sync/_flags.py +14 -6
  8. a_sync/a_sync/_helpers.py +8 -7
  9. a_sync/a_sync/_kwargs.py +3 -2
  10. a_sync/a_sync/_meta.py +120 -28
  11. a_sync/a_sync/abstract.py +102 -28
  12. a_sync/a_sync/base.py +34 -16
  13. a_sync/a_sync/config.py +47 -13
  14. a_sync/a_sync/decorator.py +239 -117
  15. a_sync/a_sync/function.py +416 -146
  16. a_sync/a_sync/method.py +197 -59
  17. a_sync/a_sync/modifiers/__init__.py +47 -5
  18. a_sync/a_sync/modifiers/cache/__init__.py +46 -17
  19. a_sync/a_sync/modifiers/cache/memory.py +86 -20
  20. a_sync/a_sync/modifiers/limiter.py +52 -22
  21. a_sync/a_sync/modifiers/manager.py +98 -16
  22. a_sync/a_sync/modifiers/semaphores.py +48 -15
  23. a_sync/a_sync/property.py +383 -82
  24. a_sync/a_sync/singleton.py +1 -0
  25. a_sync/aliases.py +0 -1
  26. a_sync/asyncio/__init__.py +4 -1
  27. a_sync/asyncio/as_completed.py +177 -49
  28. a_sync/asyncio/create_task.py +31 -17
  29. a_sync/asyncio/gather.py +72 -52
  30. a_sync/asyncio/utils.py +3 -3
  31. a_sync/exceptions.py +78 -23
  32. a_sync/executor.py +120 -71
  33. a_sync/future.py +575 -158
  34. a_sync/iter.py +110 -50
  35. a_sync/primitives/__init__.py +14 -2
  36. a_sync/primitives/_debug.py +13 -13
  37. a_sync/primitives/_loggable.py +5 -4
  38. a_sync/primitives/locks/__init__.py +5 -2
  39. a_sync/primitives/locks/counter.py +38 -36
  40. a_sync/primitives/locks/event.py +21 -7
  41. a_sync/primitives/locks/prio_semaphore.py +182 -62
  42. a_sync/primitives/locks/semaphore.py +78 -77
  43. a_sync/primitives/queue.py +560 -58
  44. a_sync/sphinx/__init__.py +0 -1
  45. a_sync/sphinx/ext.py +160 -50
  46. a_sync/task.py +262 -97
  47. a_sync/utils/__init__.py +12 -6
  48. a_sync/utils/iterators.py +127 -43
  49. {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/METADATA +1 -1
  50. ez_a_sync-0.22.15.dist-info/RECORD +74 -0
  51. {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/WHEEL +1 -1
  52. tests/conftest.py +1 -2
  53. tests/executor.py +112 -9
  54. tests/fixtures.py +61 -32
  55. tests/test_abstract.py +7 -4
  56. tests/test_as_completed.py +54 -21
  57. tests/test_base.py +66 -17
  58. tests/test_cache.py +31 -15
  59. tests/test_decorator.py +54 -28
  60. tests/test_executor.py +8 -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 +15 -10
  70. tests/test_task.py +126 -28
  71. ez_a_sync-0.22.13.dist-info/RECORD +0 -74
  72. {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/LICENSE.txt +0 -0
  73. {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/top_level.txt +0 -0
@@ -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, ModifiedMixin, ModifierManager
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
- class ASyncDescriptor(ModifiedMixin, Generic[I, P, T]):
17
+
18
+ class ASyncDescriptor(_ModifiedMixin, Generic[I, P, T]):
18
19
  """
19
- A descriptor base class for asynchronous methods and properties.
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'Unable to decorate {_fget}')
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] = self.modifiers.apply_async_modifiers(_fget)
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(self, *instances: AnyIterable[I], **bound_method_kwargs: P.kwargs) -> "TaskMapping[I, T]":
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(self, *instances: AnyIterable[I], concurrency: Optional[int] = None, name: str = "", **kwargs: P.kwargs) -> bool:
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(*instances, concurrency=concurrency, name=name, **kwargs).all(pop=True, sync=False)
154
-
155
- async def _any(self, *instances: AnyIterable[I], concurrency: Optional[int] = None, name: str = "", **kwargs: P.kwargs) -> bool:
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(*instances, concurrency=concurrency, name=name, **kwargs).any(pop=True, sync=False)
169
-
170
- async def _min(self, *instances: AnyIterable[I], concurrency: Optional[int] = None, name: str = "", **kwargs: P.kwargs) -> T:
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(*instances, concurrency=concurrency, name=name, **kwargs).min(pop=True, sync=False)
184
-
185
- async def _max(self, *instances: AnyIterable[I], concurrency: Optional[int] = None, name: str = "", **kwargs: P.kwargs) -> T:
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(*instances, concurrency=concurrency, name=name, **kwargs).max(pop=True, sync=False)
199
-
200
- async def _sum(self, *instances: AnyIterable[I], concurrency: Optional[int] = None, name: str = "", **kwargs: P.kwargs) -> T:
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(*instances, concurrency=concurrency, name=name, **kwargs).sum(pop=True, sync=False)
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 / function calls will be sync or async.
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 = {'sync'}
20
+ AFFIRMATIVE_FLAGS = {"sync"}
15
21
  """Set of flags indicating synchronous behavior."""
16
22
 
17
- NEGATIVE_FLAGS = {'asynchronous'}
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
- :class:`exceptions.InvalidFlag`: If the flag is not recognized.
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
- :class:`exceptions.InvalidFlagValue`: If the flag value is not a boolean.
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
- :class:`exceptions.SyncModeInAsyncContextError`: If the event loop is already running.
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
- :class:`exceptions.FunctionNotSync`: If the input function is already asynchronous.
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('kwargs', present_flags)
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, 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 `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("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)
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
- 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")
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("`%s.%s` starts with an underscore, skipping", new_class_name, attr_name)
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("`%s.%s` incluldes a double-underscore, skipping", new_class_name, attr_name)
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("`%s.%s` is a %s, skipping", new_class_name, attr_name, attr_value.__class__.__name__)
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(f"inspecting `{new_class_name}.{attr_name}` of type {attr_value.__class__.__name__}")
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, 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)
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("full modifier set for `%s.%s`: %s", new_class_name, attr_name, fn_modifiers)
54
- if isinstance(attr_value, (ASyncPropertyDescriptor, ASyncCachedPropertyDescriptor)):
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("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)
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("`%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)
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
- def __init__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> None:
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
- if ENVIRONMENT_VARIABLES.DEBUG_MODE or ENVIRONMENT_VARIABLES.DEBUG_CLASS_NAME == new_class_name:
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"]