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
@@ -7,34 +7,76 @@ from async_lru import alru_cache
7
7
  from a_sync import exceptions
8
8
  from a_sync._typing import *
9
9
 
10
+
10
11
  class CacheKwargs(TypedDict):
12
+ """Typed dictionary for cache keyword arguments."""
13
+
11
14
  maxsize: Optional[int]
12
15
  ttl: Optional[int]
13
16
  typed: bool
14
17
 
18
+
15
19
  @overload
16
- def apply_async_memory_cache(
17
- coro_fn: Literal[None],
18
- **kwargs: Unpack[CacheKwargs]
19
- ) -> AsyncDecorator[P, T]:...
20
-
20
+ def apply_async_memory_cache(**kwargs: Unpack[CacheKwargs]) -> AsyncDecorator[P, T]:
21
+ """Creates a decorator to apply an asynchronous LRU cache.
22
+
23
+ This overload is used when no coroutine function is provided. The returned
24
+ decorator can be applied to a coroutine function later.
25
+
26
+ Args:
27
+ **kwargs: Keyword arguments for cache configuration, including maxsize,
28
+ ttl, and typed.
29
+ """
30
+
31
+
21
32
  @overload
22
33
  def apply_async_memory_cache(
23
- coro_fn: int,
24
- **kwargs: Unpack[CacheKwargs]
25
- ) -> AsyncDecorator[P, T]:...
26
-
34
+ coro_fn: int, **kwargs: Unpack[CacheKwargs]
35
+ ) -> AsyncDecorator[P, T]:
36
+ """Creates a decorator with maxsize set by an integer.
37
+
38
+ This overload is used when an integer is provided as the coroutine function,
39
+ which sets the maxsize for the cache. The returned decorator can be applied
40
+ to a coroutine function later.
41
+
42
+ Args:
43
+ coro_fn: An integer to set as maxsize for the cache.
44
+ **kwargs: Additional keyword arguments for cache configuration, including
45
+ ttl and typed.
46
+ """
47
+
48
+
27
49
  @overload
28
50
  def apply_async_memory_cache(
29
- coro_fn: CoroFn[P, T],
30
- **kwargs: Unpack[CacheKwargs]
31
- ) -> CoroFn[P, T]:...
51
+ coro_fn: CoroFn[P, T], **kwargs: Unpack[CacheKwargs]
52
+ ) -> CoroFn[P, T]:
53
+ """Applies an asynchronous LRU cache to a provided coroutine function.
54
+
55
+ This overload is used when a coroutine function is provided. The cache is
56
+ applied directly to the function.
57
+
58
+ Args:
59
+ coro_fn: The coroutine function to be cached.
60
+ **kwargs: Keyword arguments for cache configuration, including maxsize,
61
+ ttl, and typed.
62
+ """
63
+
32
64
 
33
65
  @overload
34
66
  def apply_async_memory_cache(
35
- coro_fn: Literal[None],
36
- **kwargs: Unpack[CacheKwargs]
37
- ) -> AsyncDecorator[P, T]:...
67
+ coro_fn: Literal[None], **kwargs: Unpack[CacheKwargs]
68
+ ) -> AsyncDecorator[P, T]:
69
+ """Creates a decorator to apply an asynchronous LRU cache.
70
+
71
+ This duplicate overload is used when no coroutine function is provided. The
72
+ returned decorator can be applied to a coroutine function later.
73
+
74
+ Args:
75
+ coro_fn: None, indicating no coroutine function is provided.
76
+ **kwargs: Keyword arguments for cache configuration, including maxsize,
77
+ ttl, and typed.
78
+ """
79
+
38
80
 
39
81
  def apply_async_memory_cache(
40
82
  coro_fn: Optional[Union[CoroFn[P, T], int]] = None,
@@ -42,19 +84,43 @@ def apply_async_memory_cache(
42
84
  ttl: Optional[int] = None,
43
85
  typed: bool = False,
44
86
  ) -> AsyncDecoratorOrCoroFn[P, T]:
87
+ """Applies an asynchronous LRU cache to a coroutine function.
88
+
89
+ This function uses the `alru_cache` from the `async_lru` library to cache
90
+ the results of an asynchronous coroutine function. The cache can be configured
91
+ with a maximum size, a time-to-live (TTL), and whether the cache keys should
92
+ be typed.
93
+
94
+ Args:
95
+ coro_fn: The coroutine function to be cached, or an integer to set as maxsize.
96
+ maxsize: The maximum size of the cache. If set to -1, it is converted to None,
97
+ making the cache unbounded.
98
+ ttl: The time-to-live for cache entries in seconds. If None, entries do not expire.
99
+ typed: Whether to consider the types of arguments as part of the cache key.
100
+
101
+ Raises:
102
+ TypeError: If `maxsize` is not a positive integer or None when `coro_fn` is None.
103
+ exceptions.FunctionNotAsync: If `coro_fn` is not an asynchronous function.
104
+
105
+ Returns:
106
+ A decorator if `coro_fn` is None, otherwise the cached coroutine function.
107
+ """
45
108
  # Parse Inputs
46
109
  if isinstance(coro_fn, int):
47
110
  assert maxsize is None
48
111
  maxsize = coro_fn
49
112
  coro_fn = None
50
-
51
- # Validate
113
+
114
+ # Validate
52
115
  elif coro_fn is None:
53
- if not (maxsize is None or isinstance(maxsize, int)):
54
- raise TypeError("'lru_cache_maxsize' must be a positive integer or None.", maxsize)
116
+ if maxsize not in [None, -1] and (not isinstance(maxsize, int) or maxsize <= 0):
117
+ raise TypeError(
118
+ "'lru_cache_maxsize' must be a positive integer or None.", maxsize
119
+ )
120
+
55
121
  elif not asyncio.iscoroutinefunction(coro_fn):
56
122
  raise exceptions.FunctionNotAsync(coro_fn)
57
-
123
+
58
124
  if maxsize == -1:
59
125
  maxsize = None
60
126
 
@@ -8,48 +8,78 @@ from a_sync import aliases, exceptions
8
8
  from a_sync._typing import *
9
9
 
10
10
 
11
+ LimiterSpec = Union[int, AsyncLimiter]
12
+
13
+
11
14
  @overload
12
15
  def apply_rate_limit(
13
- coro_fn: Literal[None],
14
16
  runs_per_minute: int,
15
- ) -> AsyncDecorator[P, T]:...
16
-
17
- @overload
18
- def apply_rate_limit(
19
- coro_fn: int,
20
- runs_per_minute: Literal[None],
21
- ) -> AsyncDecorator[P, T]:...
22
-
17
+ ) -> AsyncDecorator[P, T]:
18
+ """Decorator to apply a rate limit to an asynchronous function.
19
+
20
+ Args:
21
+ runs_per_minute: The number of allowed executions per minute.
22
+ """
23
+
24
+
23
25
  @overload
24
26
  def apply_rate_limit(
25
27
  coro_fn: CoroFn[P, T],
26
- runs_per_minute: Union[int, AsyncLimiter],
27
- ) -> CoroFn[P, T]:...
28
-
28
+ runs_per_minute: LimiterSpec,
29
+ ) -> CoroFn[P, T]:
30
+ """Decorator to apply a rate limit to an asynchronous function.
31
+
32
+ Args:
33
+ coro_fn: The coroutine function to be rate-limited.
34
+ runs_per_minute: The number of allowed executions per minute or an AsyncLimiter instance.
35
+ """
36
+
37
+
29
38
  def apply_rate_limit(
30
39
  coro_fn: Optional[Union[CoroFn[P, T], int]] = None,
31
- runs_per_minute: Optional[Union[int, AsyncLimiter]] = None,
40
+ runs_per_minute: Optional[LimiterSpec] = None,
32
41
  ) -> AsyncDecoratorOrCoroFn[P, T]:
42
+ """Applies a rate limit to an asynchronous function.
43
+
44
+ This function can be used as a decorator to limit the number of times
45
+ an asynchronous function can be called per minute. It can be configured
46
+ with either an integer specifying the number of runs per minute or an
47
+ AsyncLimiter instance.
48
+
49
+ Args:
50
+ coro_fn: The coroutine function to be rate-limited. If an integer is provided, it is treated as runs per minute, and runs_per_minute should be None.
51
+ runs_per_minute: The number of allowed executions per minute or an AsyncLimiter instance. If coro_fn is an integer, this should be None.
52
+
53
+ Raises:
54
+ TypeError: If 'runs_per_minute' is neither an integer nor an AsyncLimiter when 'coro_fn' is None.
55
+ exceptions.FunctionNotAsync: If 'coro_fn' is not an asynchronous function.
56
+ """
33
57
  # Parse Inputs
34
- if isinstance(coro_fn, int):
58
+ if isinstance(coro_fn, (int, AsyncLimiter)):
35
59
  assert runs_per_minute is None
36
60
  runs_per_minute = coro_fn
37
61
  coro_fn = None
38
-
62
+
39
63
  elif coro_fn is None:
40
- if runs_per_minute is not None and not isinstance(runs_per_minute, int):
64
+ if runs_per_minute is not None and not isinstance(
65
+ runs_per_minute, (int, AsyncLimiter)
66
+ ):
41
67
  raise TypeError("'runs_per_minute' must be an integer.", runs_per_minute)
42
-
68
+
43
69
  elif not asyncio.iscoroutinefunction(coro_fn):
44
70
  raise exceptions.FunctionNotAsync(coro_fn)
45
-
71
+
46
72
  def rate_limit_decorator(coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
47
- limiter = runs_per_minute if isinstance(runs_per_minute, AsyncLimiter) else AsyncLimiter(runs_per_minute) if runs_per_minute else aliases.dummy
73
+ limiter = (
74
+ runs_per_minute
75
+ if isinstance(runs_per_minute, AsyncLimiter)
76
+ else AsyncLimiter(runs_per_minute) if runs_per_minute else aliases.dummy
77
+ )
78
+
48
79
  async def rate_limit_wrap(*args: P.args, **kwargs: P.kwargs) -> T:
49
80
  async with limiter: # type: ignore [attr-defined]
50
81
  return await coro_fn(*args, **kwargs)
82
+
51
83
  return rate_limit_wrap
52
-
84
+
53
85
  return rate_limit_decorator if coro_fn is None else rate_limit_decorator(coro_fn)
54
-
55
-
@@ -5,9 +5,20 @@ from a_sync._typing import *
5
5
  from a_sync.a_sync.config import user_set_default_modifiers, null_modifiers
6
6
  from a_sync.a_sync.modifiers import cache, limiter, semaphores
7
7
 
8
- valid_modifiers = [key for key in ModifierKwargs.__annotations__ if not key.startswith('_') and not key.endswith('_')]
8
+ valid_modifiers = [
9
+ key
10
+ for key in ModifierKwargs.__annotations__
11
+ if not key.startswith("_") and not key.endswith("_")
12
+ ]
13
+
9
14
 
10
15
  class ModifierManager(Dict[str, Any]):
16
+ """Manages modifiers for asynchronous and synchronous functions.
17
+
18
+ This class is responsible for applying modifiers to functions, such as
19
+ caching, rate limiting, and semaphores for asynchronous functions.
20
+ """
21
+
11
22
  default: DefaultMode
12
23
  cache_type: CacheType
13
24
  cache_typed: bool
@@ -17,36 +28,74 @@ class ModifierManager(Dict[str, Any]):
17
28
  semaphore: SemaphoreSpec
18
29
  # sync modifiers
19
30
  executor: Executor
20
- __slots__ = "_modifiers",
31
+ """This is not applied like a typical modifier. The executor is used to run the sync function in an asynchronous context."""
32
+
33
+ __slots__ = ("_modifiers",)
34
+
21
35
  def __init__(self, modifiers: ModifierKwargs) -> None:
36
+ """Initializes the ModifierManager with the given modifiers.
37
+
38
+ Args:
39
+ modifiers: A dictionary of modifiers to be applied.
40
+
41
+ Raises:
42
+ ValueError: If an unsupported modifier is provided.
43
+ """
22
44
  for key in modifiers.keys():
23
45
  if key not in valid_modifiers:
24
46
  raise ValueError(f"'{key}' is not a supported modifier.")
25
47
  self._modifiers = modifiers
48
+
26
49
  def __repr__(self) -> str:
50
+ """Returns a string representation of the modifiers."""
27
51
  return str(self._modifiers)
52
+
28
53
  def __getattribute__(self, modifier_key: str) -> Any:
54
+ """Gets the value of a modifier.
55
+
56
+ Args:
57
+ modifier_key: The key of the modifier to retrieve.
58
+
59
+ Returns:
60
+ The value of the modifier, or the default value if not set.
61
+ """
29
62
  if modifier_key not in valid_modifiers:
30
63
  return super().__getattribute__(modifier_key)
31
- return self[modifier_key] if modifier_key in self else user_defaults[modifier_key]
64
+ return (
65
+ self[modifier_key] if modifier_key in self else user_defaults[modifier_key]
66
+ )
32
67
 
33
-
34
68
  @property
35
69
  def use_limiter(self) -> bool:
70
+ """Determines if a rate limiter should be used."""
36
71
  return self.runs_per_minute != nulls.runs_per_minute
72
+
37
73
  @property
38
74
  def use_semaphore(self) -> bool:
75
+ """Determines if a semaphore should be used."""
39
76
  return self.semaphore != nulls.semaphore
77
+
40
78
  @property
41
79
  def use_cache(self) -> bool:
42
- return any([
43
- self.cache_type != nulls.cache_type,
44
- self.ram_cache_maxsize != nulls.ram_cache_maxsize,
45
- self.ram_cache_ttl != nulls.ram_cache_ttl,
46
- self.cache_typed != nulls.cache_typed,
47
- ])
48
-
80
+ """Determines if caching should be used."""
81
+ return any(
82
+ [
83
+ self.cache_type != nulls.cache_type,
84
+ self.ram_cache_maxsize != nulls.ram_cache_maxsize,
85
+ self.ram_cache_ttl != nulls.ram_cache_ttl,
86
+ self.cache_typed != nulls.cache_typed,
87
+ ]
88
+ )
89
+
49
90
  def apply_async_modifiers(self, coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
91
+ """Applies asynchronous modifiers to a coroutine function.
92
+
93
+ Args:
94
+ coro_fn: The coroutine function to modify.
95
+
96
+ Returns:
97
+ The modified coroutine function.
98
+ """
50
99
  # NOTE: THESE STACK IN REVERSE ORDER
51
100
  if self.use_limiter:
52
101
  coro_fn = limiter.apply_rate_limit(coro_fn, self.runs_per_minute)
@@ -55,35 +104,68 @@ class ModifierManager(Dict[str, Any]):
55
104
  if self.use_cache:
56
105
  coro_fn = cache.apply_async_cache(
57
106
  coro_fn,
58
- cache_type=self.cache_type or 'memory',
107
+ cache_type=self.cache_type or "memory",
59
108
  cache_typed=self.cache_typed,
60
109
  ram_cache_maxsize=self.ram_cache_maxsize,
61
- ram_cache_ttl=self.ram_cache_ttl
110
+ ram_cache_ttl=self.ram_cache_ttl,
62
111
  )
63
112
  return coro_fn
64
-
113
+
65
114
  def apply_sync_modifiers(self, function: SyncFn[P, T]) -> SyncFn[P, T]:
115
+ """Wraps a synchronous function.
116
+
117
+ Note:
118
+ There are no sync modifiers at this time, but they will be added here for convenience.
119
+
120
+ Args:
121
+ function: The synchronous function to wrap.
122
+
123
+ Returns:
124
+ The wrapped synchronous function.
125
+ """
126
+
66
127
  @functools.wraps(function)
67
128
  def sync_modifier_wrap(*args: P.args, **kwargs: P.kwargs) -> T:
68
129
  return function(*args, **kwargs)
69
- # NOTE There are no sync modifiers at this time but they will be added here for my convenience.
130
+
70
131
  return sync_modifier_wrap
71
-
132
+
72
133
  # Dictionary api
73
134
  def keys(self) -> KeysView[str]: # type: ignore [override]
135
+ """Returns the keys of the modifiers."""
74
136
  return self._modifiers.keys()
137
+
75
138
  def values(self) -> ValuesView[Any]: # type: ignore [override]
139
+ """Returns the values of the modifiers."""
76
140
  return self._modifiers.values()
141
+
77
142
  def items(self) -> ItemsView[str, Any]: # type: ignore [override]
143
+ """Returns the items of the modifiers."""
78
144
  return self._modifiers.items()
145
+
79
146
  def __contains__(self, key: str) -> bool: # type: ignore [override]
147
+ """Checks if a key is in the modifiers."""
80
148
  return key in self._modifiers
149
+
81
150
  def __iter__(self) -> Iterator[str]:
151
+ """Returns an iterator over the modifier keys."""
82
152
  return self._modifiers.__iter__()
153
+
83
154
  def __len__(self) -> int:
155
+ """Returns the number of modifiers."""
84
156
  return len(self._modifiers)
157
+
85
158
  def __getitem__(self, modifier_key: str):
159
+ """Gets the value of a modifier by key.
160
+
161
+ Args:
162
+ modifier_key: The key of the modifier to retrieve.
163
+
164
+ Returns:
165
+ The value of the modifier.
166
+ """
86
167
  return self._modifiers[modifier_key] # type: ignore [literal-required]
87
168
 
169
+
88
170
  nulls = ModifierManager(null_modifiers)
89
171
  user_defaults = ModifierManager(user_set_default_modifiers)
@@ -12,58 +12,91 @@ from a_sync.primitives import ThreadsafeSemaphore, DummySemaphore
12
12
 
13
13
  @overload
14
14
  def apply_semaphore( # type: ignore [misc]
15
- coro_fn: Literal[None],
16
15
  semaphore: SemaphoreSpec,
17
- ) -> AsyncDecorator[P, T]:...
16
+ ) -> AsyncDecorator[P, T]:
17
+ """Applies a semaphore to a coroutine function.
18
+
19
+ This overload is used when the semaphore is provided as a single argument,
20
+ returning a decorator that can be applied to a coroutine function.
21
+
22
+ Args:
23
+ semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object.
24
+ """
18
25
 
19
- @overload
20
- def apply_semaphore(
21
- coro_fn: SemaphoreSpec,
22
- semaphore: Literal[None],
23
- ) -> AsyncDecorator[P, T]:...
24
26
 
25
27
  @overload
26
28
  def apply_semaphore(
27
29
  coro_fn: CoroFn[P, T],
28
30
  semaphore: SemaphoreSpec,
29
- ) -> CoroFn[P, T]:...
30
-
31
+ ) -> CoroFn[P, T]:
32
+ """Applies a semaphore to a coroutine function.
33
+
34
+ This overload is used when both the coroutine function and semaphore are provided,
35
+ directly applying the semaphore to the coroutine function.
36
+
37
+ Args:
38
+ coro_fn: The coroutine function to which the semaphore will be applied.
39
+ semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object.
40
+ """
41
+
42
+
31
43
  def apply_semaphore(
32
44
  coro_fn: Optional[Union[CoroFn[P, T], SemaphoreSpec]] = None,
33
45
  semaphore: SemaphoreSpec = None,
34
46
  ) -> AsyncDecoratorOrCoroFn[P, T]:
47
+ """Applies a semaphore to a coroutine function or returns a decorator.
48
+
49
+ This function can be used to apply a semaphore to a coroutine function either by
50
+ passing the coroutine function and semaphore as arguments or by using the semaphore
51
+ as a decorator. It raises exceptions if the inputs are not valid.
52
+
53
+ Args:
54
+ coro_fn: The coroutine function to which the semaphore will be applied, or None
55
+ if the semaphore is to be used as a decorator.
56
+ semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object.
57
+
58
+ Raises:
59
+ ValueError: If both coro_fn and semaphore are provided as invalid inputs.
60
+ exceptions.FunctionNotAsync: If the provided function is not a coroutine.
61
+ TypeError: If the semaphore is not an integer or an asyncio.Semaphore object.
62
+ """
35
63
  # Parse Inputs
36
64
  if isinstance(coro_fn, (int, asyncio.Semaphore)):
37
65
  if semaphore is not None:
38
66
  raise ValueError("You can only pass in one arg.")
39
67
  semaphore = coro_fn
40
68
  coro_fn = None
41
-
69
+
42
70
  elif not asyncio.iscoroutinefunction(coro_fn):
43
71
  raise exceptions.FunctionNotAsync(coro_fn)
44
-
72
+
45
73
  # Create the semaphore if necessary
46
74
  if isinstance(semaphore, int):
47
75
  semaphore = primitives.ThreadsafeSemaphore(semaphore)
48
76
  elif not isinstance(semaphore, asyncio.Semaphore):
49
- raise TypeError(f"'semaphore' must either be an integer or a Semaphore object. You passed {semaphore}")
50
-
77
+ raise TypeError(
78
+ f"'semaphore' must either be an integer or a Semaphore object. You passed {semaphore}"
79
+ )
80
+
51
81
  # Create and return the decorator
52
82
  if isinstance(semaphore, primitives.Semaphore):
53
83
  # NOTE: Our `Semaphore` primitive can be used as a decorator.
54
84
  # While you can use it the `async with` way like any other semaphore and we could make this code section cleaner,
55
85
  # applying it as a decorator adds some useful info to its debug logs so we do that here if we can.
56
86
  semaphore_decorator = semaphore
57
-
87
+
58
88
  else:
89
+
59
90
  def semaphore_decorator(coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
60
91
  @functools.wraps(coro_fn)
61
92
  async def semaphore_wrap(*args, **kwargs) -> T:
62
93
  async with semaphore: # type: ignore [union-attr]
63
94
  return await coro_fn(*args, **kwargs)
95
+
64
96
  return semaphore_wrap
65
-
97
+
66
98
  return semaphore_decorator if coro_fn is None else semaphore_decorator(coro_fn)
67
99
 
68
100
 
69
101
  dummy_semaphore = primitives.DummySemaphore()
102
+ """A dummy semaphore that does not enforce any concurrency limits."""